@genex-ai/cli-demo 1.13.1-dev.558 → 1.14.0-dev.560

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
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- BLENDER_SETUP_HINT,
4
3
  RENDER_MODES,
5
4
  SHEET_FORMATS,
6
5
  acquireSeat,
7
6
  apiFetch,
8
7
  blenderCall,
9
8
  blenderEndpoint,
9
+ blenderSetupHint,
10
10
  fetchSignedInEmail,
11
11
  getWorkspacePath,
12
12
  isRenderMode,
@@ -22,7 +22,7 @@ import {
22
22
  writeProject,
23
23
  writeUserToken,
24
24
  writeWorkspace
25
- } from "./chunk-WXHTNSFB.js";
25
+ } from "./chunk-2UJK5OCB.js";
26
26
  import {
27
27
  CLI_CHANNEL,
28
28
  DEFAULT_API_URL,
@@ -700,11 +700,11 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
700
700
  var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
701
701
  # Genex build contract (always in effect for this game)
702
702
 
703
- Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
703
+ Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 a live Blender for composing levels and interiors \u2014 \`blender scene|exec|snap|import|export\` (hosted sessions with the GPU lane on, or \`blender serve\` on your own machine; the \`genex-blender-scene\` skill owns it) \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 load the owning row before the first file of each subsystem, and re-load the map after any compaction.
704
704
 
705
- Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Use genex capabilities as an extension, but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
705
+ Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Reach for genex capabilities by default \u2014 they are how the world gets real assets (law 22) \u2014 but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
706
706
 
707
- 1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
707
+ 1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. Before the first file of any subsystem, load its owning lane from the director's routing map and write it as \`Lane:\` beside that work in \`DESIGN.md\` (\`Lane: none \u2014 <why>\` when no row matches) \u2014 a matching row is not optional, and loading one lane never commissions another. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
708
708
  2. Ask only when one unresolved answer materially changes the work. If the request is clear, do not repeat an interview, confirm the pitch, force a concept round, or ask whole-game-vs-one-part again. For a genuinely broad new game, ask one decision: whole coordinated build or one request-relevant part first. If there is no clear request \u2014 a setup prompt pasted with nothing of their own \u2014 ask in PLAIN CHAT what they want to make, in their own words, and never fill the blank by pitching concepts. Once you know what you are building, ask one decision at a time with your built-in question tool with clickable answer options when you have one; use a short plain-chat question otherwise. If the player stays silent after a necessary question, proceed on stated assumptions where reasonable and record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
709
709
  3. \`DESIGN.md\` at the project root is the durable design contract AND build plan. Keep three truths distinct: the player's requested outcome, the current \`Now:\` focus, and open commitments. It must carry a \`## Build plan & status\` section with the working mode (\`whole coordinated build\`, \`step by step\`, or \`focused change\`), numbered milestones with status marks, and a \`Now:\` line naming the current one \u2014 a milestone is done only when its work reached a preview. The latest clear request may replace \`Now:\` immediately; it never silently shrinks the requested outcome or deletes unrelated commitments. Keep every decision, assumption, generation id/URL, local output, and wiring state current. When the plan first lands, tell the player in one plain line that you recorded what they asked for in \`DESIGN.md\` and will keep it current.
710
710
  4. Use \`genex\` commands to generate art, audio, video, characters, and UI. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\`, \`--inpaint\`, and model's \`--image\` accept local paths). Don't limit yourself: add details, and mix generated assets with procedural generation to make scenes and games detailed and lively.
@@ -713,7 +713,7 @@ Important note: put soul into your creations, with many details and love. Aim to
713
713
  7. Never draw a rectangular backing plate behind bars, digits, or icons \u2014 in sprites or CSS. Ornament lives on the widget's own silhouette; a truly needed shaped plate comes from \`npx genex ui plate\`.
714
714
  8. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
715
715
  9. Before any publish and before ending a session: run \`npx genex wait\` on every generation you enqueued and wire in what landed \u2014 never park landed assets. Fonts the brief names are LOADED for real, Escape pauses, the loader shows something of the game rather than a black screen, and the player wears the game's own generated character (or DESIGN.md records why it doesn't).
716
- 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. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead, or build it procedurally. 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.
716
+ 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. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead, or build it procedurally \u2014 held to law 22's bar like everything else the player looks at. 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.
717
717
  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.
718
718
  12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
719
719
  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. After the first release, the game is two versions and the player only ever hears these two words for them: the **draft** (their working copy, updated by \`preview\`) and the **public version** (what everyone plays). Command names are yours, not theirs \u2014 never say "promote" to the player; ask directly: "Want me to update the public version?", one line, once per round, and keep building while you wait \u2014 updating the public version is the ONE action that waits for an explicit yes, and more change requests instead of a yes mean "not yet". The player's "publish it" / "publish the update" / "update it" / "yes" after the first release ALL mean \`genex promote\` \u2014 never \`genex publish\` again, which would ship an untried rebuild. \`promote\` ships the last previewed build, so if anything was edited since the last \`preview\`, preview again before promoting.
@@ -724,6 +724,8 @@ Important note: put soul into your creations, with many details and love. Aim to
724
724
  18. NEVER delete, empty, move, rename, or overwrite anything you did not create yourself. This folder may hold the player's own reference images, notes, sketches, or an earlier attempt \u2014 files that exist nowhere else and have no undo, no trash, no backup. A non-empty folder is normal and is NEVER something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck interactive prompt, and every setup tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files") \u2014 scaffold into a fresh subfolder and copy in instead. You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes. This binds hardest during setup, where it runs fast and automatically before the player has asked for anything at all.
725
725
  19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
726
726
  20. When the player names a specific thing to integrate \u2014 a library, a controller, an asset, a part of an existing game, or a tagged reference the platform placed in \`.genex/refs/<slug>/\` \u2014 integrate the real thing: read its source first and build from it, never a lighter look-alike written from scratch because it was faster. A tagged reference owns the subsystem it was tagged for, outranking the default lane and the vendored controllers for that subsystem. If you genuinely can't use it, say so in one plain line and ask how to proceed.
727
+ 21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
728
+ 22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
727
729
  ${CONTRACT_END}
728
730
  `;
729
731
  var GENEX_TOOLS_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
@@ -2466,10 +2468,222 @@ function relTime(iso) {
2466
2468
 
2467
2469
  // src/lib/deploy.ts
2468
2470
  import "child_process";
2471
+
2472
+ // src/lib/generation-ledger.ts
2473
+ import fs12 from "fs/promises";
2474
+ import path12 from "path";
2475
+ var ledgerPath = (cwd) => path12.join(cwd, ".genex", "generations.ndjson");
2476
+ async function append(cwd, event) {
2477
+ try {
2478
+ await fs12.access(path12.join(cwd, ".genex"));
2479
+ await fs12.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
2480
+ `, "utf8");
2481
+ } catch {
2482
+ }
2483
+ }
2484
+ async function readLedger(cwd = process.cwd()) {
2485
+ let raw;
2486
+ try {
2487
+ raw = await fs12.readFile(ledgerPath(cwd), "utf8");
2488
+ } catch {
2489
+ return [];
2490
+ }
2491
+ const entries = /* @__PURE__ */ new Map();
2492
+ for (const line of raw.split("\n")) {
2493
+ if (!line.trim()) continue;
2494
+ try {
2495
+ const e = JSON.parse(line);
2496
+ if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
2497
+ if (!entries.has(e.id)) {
2498
+ entries.set(e.id, {
2499
+ id: e.id,
2500
+ kind: e.kind,
2501
+ prompt: typeof e.prompt === "string" ? e.prompt : "",
2502
+ ...typeof e.tail === "string" ? { promptTail: e.tail } : {},
2503
+ queuedAt: typeof e.at === "string" ? e.at : "",
2504
+ status: "queued"
2505
+ });
2506
+ }
2507
+ } else if (e.t === "done" && typeof e.id === "string") {
2508
+ const entry = entries.get(e.id);
2509
+ if (entry && (e.status === "completed" || e.status === "failed")) {
2510
+ entry.status = e.status;
2511
+ if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
2512
+ }
2513
+ }
2514
+ } catch {
2515
+ }
2516
+ }
2517
+ return [...entries.values()];
2518
+ }
2519
+ async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
2520
+ await append(cwd, {
2521
+ t: "q",
2522
+ id,
2523
+ kind,
2524
+ prompt: prompt.slice(0, 120),
2525
+ ...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
2526
+ at: (/* @__PURE__ */ new Date()).toISOString()
2527
+ });
2528
+ }
2529
+ async function recordTerminal(id, status, urls, cwd = process.cwd()) {
2530
+ const existing = (await readLedger(cwd)).find((e) => e.id === id);
2531
+ if (existing && existing.status === status) return;
2532
+ await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
2533
+ }
2534
+ async function countFailed(kind, cwd = process.cwd()) {
2535
+ return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
2536
+ }
2537
+ async function countOutcomes(kind, cwd = process.cwd()) {
2538
+ const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
2539
+ return {
2540
+ failed: rows.filter((e) => e.status === "failed").length,
2541
+ completed: rows.filter((e) => e.status === "completed").length
2542
+ };
2543
+ }
2544
+
2545
+ // src/lib/generation-plan.ts
2546
+ var LANE_OF_KIND = {
2547
+ image: "image",
2548
+ video: "video",
2549
+ sfx: "sfx",
2550
+ music: "music",
2551
+ voice: "voice",
2552
+ texture: "texture",
2553
+ skybox: "skybox",
2554
+ model: "model",
2555
+ model_segment: "model",
2556
+ model_rig: "model",
2557
+ model_animation: "model",
2558
+ character: "character",
2559
+ character_concept: "character",
2560
+ character_preview: "character",
2561
+ character_animation: "character",
2562
+ character_motion: "character",
2563
+ motion: "motion"
2564
+ };
2565
+ var LANE_OF_SKILL = {
2566
+ "genex-ai-model": "model",
2567
+ "genex-ai-texture": "texture",
2568
+ "genex-ai-image": "image",
2569
+ "genex-ai-video": "video",
2570
+ "genex-ai-sfx": "sfx",
2571
+ "genex-ai-music": "music",
2572
+ "genex-ai-voice": "voice",
2573
+ "genex-ai-character": "character",
2574
+ "genex-ai-hud": "image",
2575
+ "genex-ai-menu": "image"
2576
+ };
2577
+ var LANE_COMMAND = {
2578
+ model: "npx genex model",
2579
+ texture: "npx genex texture",
2580
+ image: "npx genex image",
2581
+ video: "npx genex video",
2582
+ sfx: "npx genex sfx",
2583
+ music: "npx genex music",
2584
+ voice: "npx genex voice",
2585
+ character: "npx genex character",
2586
+ motion: 'npx genex character animate <id> "<verb>"'
2587
+ };
2588
+ var LANE_TOKEN = /\$(genex-[a-z0-9-]+)/g;
2589
+ var PROCEDURAL_ROW = /procedural|code-built|hand-built|three\.?js|\.tsx?\b|building \(/i;
2590
+ var PAID_ROW = /\b(model|glb|mesh|texture|image|sprite|poster|decal|video|sfx|sound|music|track|voice|character|creature|motion)\b/i;
2591
+ var LANE_WORDS = [
2592
+ [/\b(model|glb|mesh)\b/i, "model"],
2593
+ [/\btexture\b/i, "texture"],
2594
+ [/\b(image|sprite|poster|decal)\b/i, "image"],
2595
+ [/\bvideo\b/i, "video"],
2596
+ [/\b(sfx|sound)\b/i, "sfx"],
2597
+ [/\b(music|track)\b/i, "music"],
2598
+ [/\bvoice\b/i, "voice"],
2599
+ [/\b(character|creature)\b/i, "character"],
2600
+ [/\bmotion\b/i, "motion"]
2601
+ ];
2602
+ var GENERATION_NONE = /^\s*Generation:\s*none\s*—/im;
2603
+ var STATUS = /\b(proposed|planned|generating|landed|wired)\b/i;
2604
+ function sectionRows(design, heading) {
2605
+ const lines = design.split("\n");
2606
+ const start = lines.findIndex((l) => heading.test(l));
2607
+ if (start < 0) return [];
2608
+ const rows = [];
2609
+ for (let i = start + 1; i < lines.length; i += 1) {
2610
+ const line = lines[i] ?? "";
2611
+ if (/^##\s/.test(line)) break;
2612
+ if (!line.trim().startsWith("|")) continue;
2613
+ const cells = line.split("|").map((c2) => c2.trim()).filter((c2) => c2.length > 0);
2614
+ if (cells.length === 0 || cells.every((c2) => /^:?-+:?$/.test(c2))) continue;
2615
+ if (/^asset$/i.test(cells[0] ?? "") || /^module$/i.test(cells[0] ?? "")) continue;
2616
+ rows.push(cells);
2617
+ }
2618
+ return rows;
2619
+ }
2620
+ function classify(cells) {
2621
+ const name = cells[0] ?? "";
2622
+ if (/^<.*>$/.test(name)) return null;
2623
+ const rest = cells.slice(1).join(" ");
2624
+ const status = (STATUS.exec(rest) ?? [""])[0].toLowerCase();
2625
+ if (PROCEDURAL_ROW.test(rest)) return { name, lane: null, status, procedural: true };
2626
+ const token = new RegExp(LANE_TOKEN.source).exec(rest)?.[1];
2627
+ const fromSkill = token ? LANE_OF_SKILL[token] : void 0;
2628
+ if (fromSkill) return { name, lane: fromSkill, status, procedural: false };
2629
+ if (!PAID_ROW.test(rest) && !/https?:\/\//.test(rest) && !/generating\s*\(/i.test(rest)) return null;
2630
+ const lane = LANE_WORDS.find(([re]) => re.test(rest))?.[1] ?? null;
2631
+ return { name, lane, status, procedural: false };
2632
+ }
2633
+ function auditGenerationPlan(input) {
2634
+ const { design, ledger } = input;
2635
+ if (design.trim().length === 0) return [];
2636
+ const assetRows = sectionRows(design, /^##\s+Assets\b/i);
2637
+ const proceduralRows = sectionRows(design, /^##\s+Procedural assets\b/i);
2638
+ const rows = [
2639
+ ...assetRows.map(classify),
2640
+ ...proceduralRows.map((c2) => {
2641
+ const r = classify(c2);
2642
+ return r === null ? null : { ...r, lane: null, procedural: true };
2643
+ })
2644
+ ].filter((r) => r !== null);
2645
+ const declaredLanes = /* @__PURE__ */ new Set();
2646
+ for (const m of design.matchAll(LANE_TOKEN)) {
2647
+ const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
2648
+ if (lane) declaredLanes.add(lane);
2649
+ }
2650
+ const lanesRun = new Set(ledger.map((e) => LANE_OF_KIND[e.kind] ?? e.kind));
2651
+ const warnings = [];
2652
+ const owed = /* @__PURE__ */ new Map();
2653
+ for (const row of rows) {
2654
+ if (row.procedural || row.lane === null) continue;
2655
+ if (row.status !== "planned" && row.status !== "generating") continue;
2656
+ if (lanesRun.has(row.lane)) continue;
2657
+ if (row.lane === "skybox") continue;
2658
+ const held = owed.get(row.lane) ?? [];
2659
+ held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
2660
+ owed.set(row.lane, held);
2661
+ }
2662
+ for (const lane of declaredLanes) {
2663
+ if (lanesRun.has(lane) || owed.has(lane) || lane === "skybox") continue;
2664
+ if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
2665
+ }
2666
+ for (const [lane, names] of owed) {
2667
+ const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? `, +${names.length - 3} more` : "");
2668
+ warnings.push(
2669
+ `DESIGN.md plans generated ${lane} work (${shown}) and this project has never run a ${lane} generation \u2014 the plan says so and the generation ledger has nothing. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
2670
+ );
2671
+ }
2672
+ const paidRows = rows.filter((r) => !r.procedural);
2673
+ const codeRows = rows.filter((r) => r.procedural);
2674
+ if (paidRows.length === 0 && codeRows.length >= 1 && ledger.length === 0 && !GENERATION_NONE.test(design)) {
2675
+ warnings.push(
2676
+ `DESIGN.md's asset plan is all code-built rows and this project has never run a generation. What the player sees must read as the thing it is (build contract, law 22): a coloured box or a capsule is not a finished building, person, or prop. Generate what the request names and what the player looks at up close (npx genex model / texture / music \u2026 --no-wait), or record "Generation: none \u2014 <why code alone reaches the bar here>" beside the table.`
2677
+ );
2678
+ }
2679
+ return warnings;
2680
+ }
2681
+
2682
+ // src/lib/deploy.ts
2469
2683
  import crypto3 from "crypto";
2470
- import fs14 from "fs/promises";
2684
+ import fs15 from "fs/promises";
2471
2685
  import os8 from "os";
2472
- import path13 from "path";
2686
+ import path14 from "path";
2473
2687
 
2474
2688
  // ../../packages/mobile-scan/src/image-dims.ts
2475
2689
  function u32be(b, o) {
@@ -2733,12 +2947,12 @@ function tierFor(estVramMb) {
2733
2947
  }
2734
2948
 
2735
2949
  // src/commands/ui.ts
2736
- import fs13 from "fs/promises";
2737
- import path12 from "path";
2950
+ import fs14 from "fs/promises";
2951
+ import path13 from "path";
2738
2952
  import { PNG as PNG2 } from "pngjs";
2739
2953
 
2740
2954
  // src/lib/png-tools.ts
2741
- import fs12 from "fs/promises";
2955
+ import fs13 from "fs/promises";
2742
2956
  import { PNG } from "pngjs";
2743
2957
  var ALPHA_TRANSPARENT_MAX = 16;
2744
2958
  var isHttpUrl = (s) => /^https?:\/\//i.test(s);
@@ -2749,12 +2963,12 @@ async function loadPng(input) {
2749
2963
  if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
2750
2964
  buf = Buffer.from(await res.arrayBuffer());
2751
2965
  } else {
2752
- buf = await fs12.readFile(input);
2966
+ buf = await fs13.readFile(input);
2753
2967
  }
2754
2968
  return PNG.sync.read(buf);
2755
2969
  }
2756
2970
  async function writePng(file, png) {
2757
- await fs12.writeFile(file, PNG.sync.write(png));
2971
+ await fs13.writeFile(file, PNG.sync.write(png));
2758
2972
  }
2759
2973
  function cropPng(image, box) {
2760
2974
  const out = new PNG({ width: box.w, height: box.h });
@@ -3054,7 +3268,7 @@ async function uiExtract(opts, log) {
3054
3268
  const dilatePx = opts.dilate ?? 0;
3055
3269
  const sheet = await loadPng(input);
3056
3270
  const { width: W, height: H, data } = sheet;
3057
- await fs13.mkdir(outDir, { recursive: true });
3271
+ await fs14.mkdir(outDir, { recursive: true });
3058
3272
  log.plain(c.bold("genex ui extract"));
3059
3273
  log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
3060
3274
  let hasTransparency = false;
@@ -3283,7 +3497,7 @@ async function uiExtract(opts, log) {
3283
3497
  rimPixels: speckle.sampled
3284
3498
  });
3285
3499
  }
3286
- const outPath = path12.join(outDir, `${name}.png`);
3500
+ const outPath = path13.join(outDir, `${name}.png`);
3287
3501
  await writePng(outPath, out);
3288
3502
  const sidecar = {
3289
3503
  name,
@@ -3302,7 +3516,7 @@ async function uiExtract(opts, log) {
3302
3516
  defringed
3303
3517
  };
3304
3518
  const { name: _n, out: _o, ...sidecarBody } = sidecar;
3305
- await fs13.writeFile(
3519
+ await fs14.writeFile(
3306
3520
  outPath.replace(/\.png$/i, "") + ".bbox.json",
3307
3521
  JSON.stringify(sidecarBody, null, 2)
3308
3522
  );
@@ -3311,8 +3525,8 @@ async function uiExtract(opts, log) {
3311
3525
  `${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
3312
3526
  );
3313
3527
  }
3314
- const debugPath = path12.join(outDir, "extract-debug.json");
3315
- await fs13.writeFile(
3528
+ const debugPath = path13.join(outDir, "extract-debug.json");
3529
+ await fs14.writeFile(
3316
3530
  debugPath,
3317
3531
  JSON.stringify(
3318
3532
  {
@@ -3774,7 +3988,7 @@ async function uiMasks(opts, log) {
3774
3988
  const registrationTolerance = opts.registrationTolerance ?? 0.04;
3775
3989
  const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
3776
3990
  const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
3777
- await fs13.mkdir(outDir, { recursive: true });
3991
+ await fs14.mkdir(outDir, { recursive: true });
3778
3992
  log.plain(c.bold("genex ui masks"));
3779
3993
  log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
3780
3994
  const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
@@ -3827,11 +4041,11 @@ async function uiMasks(opts, log) {
3827
4041
  });
3828
4042
  }
3829
4043
  const overlay = makeOverlay(clean2, converted.png);
3830
- const framePath = path12.join(outDir, `${pair.name}-frame.png`);
3831
- const maskPath = path12.join(outDir, `${pair.name}-mask.png`);
3832
- const annotatedPath = path12.join(outDir, `${pair.name}-annotated-source.png`);
3833
- const overlayPath = path12.join(outDir, `${pair.name}-overlay.png`);
3834
- const metaPath = path12.join(outDir, `${pair.name}.annotated-progress.json`);
4044
+ const framePath = path13.join(outDir, `${pair.name}-frame.png`);
4045
+ const maskPath = path13.join(outDir, `${pair.name}-mask.png`);
4046
+ const annotatedPath = path13.join(outDir, `${pair.name}-annotated-source.png`);
4047
+ const overlayPath = path13.join(outDir, `${pair.name}-overlay.png`);
4048
+ const metaPath = path13.join(outDir, `${pair.name}.annotated-progress.json`);
3835
4049
  await writePng(framePath, clean2);
3836
4050
  await writePng(maskPath, converted.png);
3837
4051
  await writePng(annotatedPath, annotated);
@@ -3878,7 +4092,7 @@ async function uiMasks(opts, log) {
3878
4092
  },
3879
4093
  overlay: overlayPath
3880
4094
  };
3881
- await fs13.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
4095
+ await fs14.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
3882
4096
  `);
3883
4097
  results.push(meta);
3884
4098
  const fb = converted.bbox;
@@ -3894,8 +4108,8 @@ async function uiMasks(opts, log) {
3894
4108
  );
3895
4109
  }
3896
4110
  }
3897
- const indexPath = path12.join(outDir, "annotated-progress.json");
3898
- await fs13.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
4111
+ const indexPath = path13.join(outDir, "annotated-progress.json");
4112
+ await fs14.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
3899
4113
  `);
3900
4114
  log.plain("");
3901
4115
  log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
@@ -4021,7 +4235,7 @@ async function uiTextColor(opts, log) {
4021
4235
  };
4022
4236
  process.stdout.write(`${JSON.stringify(result, null, 2)}
4023
4237
  `);
4024
- if (opts.out) await fs13.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
4238
+ if (opts.out) await fs14.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
4025
4239
  `);
4026
4240
  if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
4027
4241
  }
@@ -4053,7 +4267,7 @@ async function uiTrim(opts, log) {
4053
4267
  const sidecar = computeBBoxes(trimmed);
4054
4268
  const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
4055
4269
  const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
4056
- await fs13.writeFile(
4270
+ await fs14.writeFile(
4057
4271
  sidecarPath,
4058
4272
  JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
4059
4273
  );
@@ -4160,7 +4374,7 @@ async function uiPlate(opts, log) {
4160
4374
  fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
4161
4375
  }
4162
4376
  await writePng(outPath, out);
4163
- const name = path12.basename(outPath);
4377
+ const name = path13.basename(outPath);
4164
4378
  log.plain(c.bold("genex ui plate"));
4165
4379
  log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
4166
4380
  log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
@@ -4325,13 +4539,13 @@ async function walkFiles(dir) {
4325
4539
  const out = [];
4326
4540
  let entries;
4327
4541
  try {
4328
- entries = await fs13.readdir(dir, { withFileTypes: true });
4542
+ entries = await fs14.readdir(dir, { withFileTypes: true });
4329
4543
  } catch {
4330
4544
  return out;
4331
4545
  }
4332
4546
  for (const entry of entries) {
4333
4547
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
4334
- const p = path12.join(dir, entry.name);
4548
+ const p = path13.join(dir, entry.name);
4335
4549
  if (entry.isDirectory()) out.push(...await walkFiles(p));
4336
4550
  else out.push(p);
4337
4551
  }
@@ -4340,7 +4554,7 @@ async function walkFiles(dir) {
4340
4554
  async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4341
4555
  const viewportFindings = [];
4342
4556
  try {
4343
- const indexHtml = await fs13.readFile(path12.join(cwd, "index.html"), "utf8");
4557
+ const indexHtml = await fs14.readFile(path13.join(cwd, "index.html"), "utf8");
4344
4558
  if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
4345
4559
  viewportFindings.push({
4346
4560
  kind: "viewport-meta",
@@ -4354,28 +4568,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4354
4568
  }
4355
4569
  } catch {
4356
4570
  }
4357
- const absAssets = path12.resolve(cwd, assetDir);
4571
+ const absAssets = path13.resolve(cwd, assetDir);
4358
4572
  try {
4359
- if (!(await fs13.stat(absAssets)).isDirectory()) {
4573
+ if (!(await fs14.stat(absAssets)).isDirectory()) {
4360
4574
  return viewportFindings.length > 0 ? viewportFindings : null;
4361
4575
  }
4362
4576
  } catch {
4363
4577
  return viewportFindings.length > 0 ? viewportFindings : null;
4364
4578
  }
4365
- const srcFiles = (await walkFiles(path12.resolve(cwd, srcDir))).filter(
4366
- (p) => AUDIT_SRC_EXTS.has(path12.extname(p).toLowerCase())
4579
+ const srcFiles = (await walkFiles(path13.resolve(cwd, srcDir))).filter(
4580
+ (p) => AUDIT_SRC_EXTS.has(path13.extname(p).toLowerCase())
4367
4581
  );
4368
4582
  try {
4369
- for (const name of await fs13.readdir(cwd)) {
4370
- const ext = path12.extname(name).toLowerCase();
4371
- if (ext === ".html" || ext === ".css") srcFiles.push(path12.join(cwd, name));
4583
+ for (const name of await fs14.readdir(cwd)) {
4584
+ const ext = path13.extname(name).toLowerCase();
4585
+ if (ext === ".html" || ext === ".css") srcFiles.push(path13.join(cwd, name));
4372
4586
  }
4373
4587
  } catch {
4374
4588
  }
4375
4589
  const sources = [];
4376
4590
  for (const p of srcFiles) {
4377
4591
  try {
4378
- sources.push({ rel: path12.relative(cwd, p), text: await fs13.readFile(p, "utf8") });
4592
+ sources.push({ rel: path13.relative(cwd, p), text: await fs14.readFile(p, "utf8") });
4379
4593
  } catch {
4380
4594
  }
4381
4595
  }
@@ -4385,11 +4599,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4385
4599
  const metaByName = /* @__PURE__ */ new Map();
4386
4600
  const bboxByPng = /* @__PURE__ */ new Map();
4387
4601
  for (const p of assetFiles) {
4388
- const base = path12.basename(p);
4602
+ const base = path13.basename(p);
4389
4603
  const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
4390
4604
  if (metaMatch) {
4391
4605
  try {
4392
- const meta = JSON.parse(await fs13.readFile(p, "utf8"));
4606
+ const meta = JSON.parse(await fs14.readFile(p, "utf8"));
4393
4607
  metaByName.set(metaMatch[1], {
4394
4608
  cleanCrop: meta.clean?.crop ?? null,
4395
4609
  loosened: meta.loosened === true
@@ -4400,7 +4614,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4400
4614
  }
4401
4615
  if (base.endsWith(".bbox.json")) {
4402
4616
  try {
4403
- const sidecar = JSON.parse(await fs13.readFile(p, "utf8"));
4617
+ const sidecar = JSON.parse(await fs14.readFile(p, "utf8"));
4404
4618
  if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
4405
4619
  } catch {
4406
4620
  }
@@ -4409,7 +4623,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4409
4623
  for (const [name, meta] of metaByName) {
4410
4624
  if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
4411
4625
  for (const p of assetFiles) {
4412
- const base = path12.basename(p);
4626
+ const base = path13.basename(p);
4413
4627
  if (!base.toLowerCase().endsWith(".png")) continue;
4414
4628
  if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
4415
4629
  if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
@@ -4433,7 +4647,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4433
4647
  }
4434
4648
  const pngByBase = /* @__PURE__ */ new Map();
4435
4649
  for (const p of assetFiles) {
4436
- const base = path12.basename(p);
4650
+ const base = path13.basename(p);
4437
4651
  if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
4438
4652
  }
4439
4653
  for (const [maskBase, maskPath] of pngByBase) {
@@ -4446,8 +4660,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4446
4660
  let frame;
4447
4661
  let mask;
4448
4662
  try {
4449
- frame = PNG2.sync.read(await fs13.readFile(framePath));
4450
- mask = PNG2.sync.read(await fs13.readFile(maskPath));
4663
+ frame = PNG2.sync.read(await fs14.readFile(framePath));
4664
+ mask = PNG2.sync.read(await fs14.readFile(maskPath));
4451
4665
  } catch {
4452
4666
  continue;
4453
4667
  }
@@ -4466,7 +4680,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4466
4680
  if (!referenced(base)) continue;
4467
4681
  let png;
4468
4682
  try {
4469
- png = PNG2.sync.read(await fs13.readFile(p));
4683
+ png = PNG2.sync.read(await fs14.readFile(p));
4470
4684
  } catch {
4471
4685
  continue;
4472
4686
  }
@@ -4486,7 +4700,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4486
4700
  }
4487
4701
  const maskReported = /* @__PURE__ */ new Set();
4488
4702
  for (const p of assetFiles) {
4489
- const m = /^(.+)\.annotated-progress\.json$/.exec(path12.basename(p));
4703
+ const m = /^(.+)\.annotated-progress\.json$/.exec(path13.basename(p));
4490
4704
  if (!m) continue;
4491
4705
  const maskBase = `${m[1]}-mask.png`;
4492
4706
  if (!referenced(maskBase)) {
@@ -4498,14 +4712,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
4498
4712
  }
4499
4713
  }
4500
4714
  for (const p of assetFiles) {
4501
- const base = path12.basename(p);
4715
+ const base = path13.basename(p);
4502
4716
  if (!base.toLowerCase().endsWith(".png")) continue;
4503
4717
  if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
4504
4718
  if (maskReported.has(base)) continue;
4505
4719
  if (!referenced(base)) {
4506
4720
  findings.push({
4507
4721
  kind: "unwired-sprite",
4508
- message: `${path12.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
4722
+ message: `${path13.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
4509
4723
  });
4510
4724
  }
4511
4725
  }
@@ -4692,7 +4906,7 @@ async function printUiAuditPreflight(log) {
4692
4906
  }
4693
4907
  async function printPipelineStatePreflight(log, cwd = process.cwd()) {
4694
4908
  try {
4695
- const design = await fs14.readFile(path13.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
4909
+ const design = await fs15.readFile(path14.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
4696
4910
  const warnings = [];
4697
4911
  if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
4698
4912
  warnings.push(
@@ -4700,13 +4914,14 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
4700
4914
  );
4701
4915
  }
4702
4916
  if (!/player character:/i.test(design)) {
4703
- const hasCharacter = await fs14.access(path13.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
4917
+ const hasCharacter = await fs15.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
4704
4918
  if (!hasCharacter && await loadsPlayerBody(cwd)) {
4705
4919
  warnings.push(
4706
4920
  `Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
4707
4921
  );
4708
4922
  }
4709
4923
  }
4924
+ warnings.push(...auditGenerationPlan({ design, ledger: await readLedger(cwd) }));
4710
4925
  for (const w of warnings) log.warn(w);
4711
4926
  } catch {
4712
4927
  }
@@ -4714,12 +4929,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
4714
4929
  async function loadsPlayerBody(cwd) {
4715
4930
  const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
4716
4931
  try {
4717
- const entries = await fs14.readdir(path13.join(cwd, "src"), { recursive: true });
4932
+ const entries = await fs15.readdir(path14.join(cwd, "src"), { recursive: true });
4718
4933
  for (const rel of entries) {
4719
4934
  if (rel.includes("node_modules")) continue;
4720
- if (rel.split(path13.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
4935
+ if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
4721
4936
  if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
4722
- const text = await fs14.readFile(path13.join(cwd, "src", rel), "utf8").catch(() => "");
4937
+ const text = await fs15.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
4723
4938
  if (BODY_LOADERS.test(text)) return true;
4724
4939
  }
4725
4940
  } catch {
@@ -4756,9 +4971,9 @@ async function deployGame(ctx, opts, log) {
4756
4971
  }
4757
4972
  log.success("Built.");
4758
4973
  }
4759
- const distDir = path13.join(cwd, "dist");
4974
+ const distDir = path14.join(cwd, "dist");
4760
4975
  const siteDir = await isDir(distDir) ? distDir : cwd;
4761
- const rel = path13.relative(cwd, siteDir) || ".";
4976
+ const rel = path14.relative(cwd, siteDir) || ".";
4762
4977
  if (siteDir === cwd) await writeGitignore(cwd, log);
4763
4978
  const files = await collectFiles(siteDir);
4764
4979
  if (files.length === 0) {
@@ -4847,7 +5062,7 @@ async function deployGame(ctx, opts, log) {
4847
5062
  }
4848
5063
  async function hasBuildScript(cwd) {
4849
5064
  try {
4850
- const pkg = JSON.parse(await fs14.readFile(path13.join(cwd, "package.json"), "utf8"));
5065
+ const pkg = JSON.parse(await fs15.readFile(path14.join(cwd, "package.json"), "utf8"));
4851
5066
  return Boolean(pkg.scripts?.build);
4852
5067
  } catch {
4853
5068
  return false;
@@ -4856,12 +5071,12 @@ async function hasBuildScript(cwd) {
4856
5071
  async function collectFiles(root) {
4857
5072
  const out = [];
4858
5073
  const walk2 = async (dir, prefix) => {
4859
- for (const e of await fs14.readdir(dir, { withFileTypes: true })) {
5074
+ for (const e of await fs15.readdir(dir, { withFileTypes: true })) {
4860
5075
  const relPath = prefix ? `${prefix}/${e.name}` : e.name;
4861
5076
  if (e.isDirectory()) {
4862
- if (!EXCLUDE_DIRS.has(e.name)) await walk2(path13.join(dir, e.name), relPath);
5077
+ if (!EXCLUDE_DIRS.has(e.name)) await walk2(path14.join(dir, e.name), relPath);
4863
5078
  } else if (e.isFile() && !isSecretEnvFile(e.name)) {
4864
- out.push({ relPath, bytes: await fs14.readFile(path13.join(dir, e.name)) });
5079
+ out.push({ relPath, bytes: await fs15.readFile(path14.join(dir, e.name)) });
4865
5080
  }
4866
5081
  }
4867
5082
  };
@@ -5103,7 +5318,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommi
5103
5318
  log.error("Couldn't save your game's source \u2014 please try again.");
5104
5319
  return false;
5105
5320
  };
5106
- const gitDir = await fs14.mkdtemp(path13.join(os8.tmpdir(), "genex-source-"));
5321
+ const gitDir = await fs15.mkdtemp(path14.join(os8.tmpdir(), "genex-source-"));
5107
5322
  const base = { GIT_DIR: gitDir };
5108
5323
  if (urlHasEmbeddedCredentials(pushUrl)) {
5109
5324
  base.GIT_CONFIG_COUNT = "1";
@@ -5118,12 +5333,12 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommi
5118
5333
  };
5119
5334
  try {
5120
5335
  if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
5121
- await fs14.writeFile(
5122
- path13.join(gitDir, "info", "exclude"),
5336
+ await fs15.writeFile(
5337
+ path14.join(gitDir, "info", "exclude"),
5123
5338
  // .env* are secrets — never publish them; `!` keeps the non-secret template.
5124
5339
  ["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
5125
5340
  );
5126
- const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path13.join(gitDir, "index-source") };
5341
+ const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path14.join(gitDir, "index-source") };
5127
5342
  let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
5128
5343
  if (!lfs) {
5129
5344
  log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
@@ -5175,7 +5390,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommi
5175
5390
  } catch {
5176
5391
  return failed();
5177
5392
  } finally {
5178
- await fs14.rm(gitDir, { recursive: true, force: true }).catch(() => {
5393
+ await fs15.rm(gitDir, { recursive: true, force: true }).catch(() => {
5179
5394
  });
5180
5395
  }
5181
5396
  }
@@ -5227,7 +5442,7 @@ async function fetchPushUrl(ctx, log, expectStagingCommit) {
5227
5442
  }
5228
5443
  async function isDir(p) {
5229
5444
  try {
5230
- return (await fs14.stat(p)).isDirectory();
5445
+ return (await fs15.stat(p)).isDirectory();
5231
5446
  } catch {
5232
5447
  return false;
5233
5448
  }
@@ -5711,81 +5926,6 @@ async function promoteBuild(apiUrl, projectId, token, log) {
5711
5926
  // src/lib/detect-features.ts
5712
5927
  import fs16 from "fs/promises";
5713
5928
  import path15 from "path";
5714
-
5715
- // src/lib/generation-ledger.ts
5716
- import fs15 from "fs/promises";
5717
- import path14 from "path";
5718
- var ledgerPath = (cwd) => path14.join(cwd, ".genex", "generations.ndjson");
5719
- async function append(cwd, event) {
5720
- try {
5721
- await fs15.access(path14.join(cwd, ".genex"));
5722
- await fs15.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
5723
- `, "utf8");
5724
- } catch {
5725
- }
5726
- }
5727
- async function readLedger(cwd = process.cwd()) {
5728
- let raw;
5729
- try {
5730
- raw = await fs15.readFile(ledgerPath(cwd), "utf8");
5731
- } catch {
5732
- return [];
5733
- }
5734
- const entries = /* @__PURE__ */ new Map();
5735
- for (const line of raw.split("\n")) {
5736
- if (!line.trim()) continue;
5737
- try {
5738
- const e = JSON.parse(line);
5739
- if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
5740
- if (!entries.has(e.id)) {
5741
- entries.set(e.id, {
5742
- id: e.id,
5743
- kind: e.kind,
5744
- prompt: typeof e.prompt === "string" ? e.prompt : "",
5745
- ...typeof e.tail === "string" ? { promptTail: e.tail } : {},
5746
- queuedAt: typeof e.at === "string" ? e.at : "",
5747
- status: "queued"
5748
- });
5749
- }
5750
- } else if (e.t === "done" && typeof e.id === "string") {
5751
- const entry = entries.get(e.id);
5752
- if (entry && (e.status === "completed" || e.status === "failed")) {
5753
- entry.status = e.status;
5754
- if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
5755
- }
5756
- }
5757
- } catch {
5758
- }
5759
- }
5760
- return [...entries.values()];
5761
- }
5762
- async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
5763
- await append(cwd, {
5764
- t: "q",
5765
- id,
5766
- kind,
5767
- prompt: prompt.slice(0, 120),
5768
- ...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
5769
- at: (/* @__PURE__ */ new Date()).toISOString()
5770
- });
5771
- }
5772
- async function recordTerminal(id, status, urls, cwd = process.cwd()) {
5773
- const existing = (await readLedger(cwd)).find((e) => e.id === id);
5774
- if (existing && existing.status === status) return;
5775
- await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
5776
- }
5777
- async function countFailed(kind, cwd = process.cwd()) {
5778
- return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
5779
- }
5780
- async function countOutcomes(kind, cwd = process.cwd()) {
5781
- const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
5782
- return {
5783
- failed: rows.filter((e) => e.status === "failed").length,
5784
- completed: rows.filter((e) => e.status === "completed").length
5785
- };
5786
- }
5787
-
5788
- // src/lib/detect-features.ts
5789
5929
  async function detectEmbedSdkVersion(cwd = process.cwd()) {
5790
5930
  try {
5791
5931
  const raw = await fs16.readFile(path15.join(cwd, "package.json"), "utf8");
@@ -19391,7 +19531,7 @@ var EXPECT_DIR = {
19391
19531
  bl: [D, -D],
19392
19532
  br: [-D, -D]
19393
19533
  };
19394
- function classify(stemRaw) {
19534
+ function classify2(stemRaw) {
19395
19535
  const stem = stemRaw.replace(/-s\d+$/, "");
19396
19536
  if (stem.includes("jump") || stem.includes("ollie") || stem.includes("trick")) {
19397
19537
  return { kind: "oneshot", expect: null };
@@ -19782,7 +19922,7 @@ function score(kind, m, gates = DEFAULT_GATES) {
19782
19922
  }
19783
19923
  function analyzeTake(stem, buf, gates = DEFAULT_GATES) {
19784
19924
  const t = loadTake(buf);
19785
- const { kind, expect } = classify(stem);
19925
+ const { kind, expect } = classify2(stem);
19786
19926
  const F = t.F;
19787
19927
  const lo = kind !== "oneshot" ? Math.min(RAMP, Math.max(4, Math.floor(F / 4))) : 8;
19788
19928
  const w = [lo, F - Math.min(TAIL, Math.max(2, Math.floor(F / 8)))];
@@ -20047,7 +20187,7 @@ function compileSet(inputs, setName, cfg = DEFAULT_MOTION_CONFIG) {
20047
20187
  const gaits = {};
20048
20188
  const aimTakes = [];
20049
20189
  for (const { stem, take } of inputs) {
20050
- const { kind } = classify(stem);
20190
+ const { kind } = classify2(stem);
20051
20191
  const aim = /aim|rifle|pistol/.test(stem);
20052
20192
  if (aim && (kind === "idle" || kind === "freeform")) aimTakes.push({ stem, take });
20053
20193
  if (kind === "loco") {
@@ -20473,7 +20613,7 @@ async function runMotion(opts) {
20473
20613
  // src/commands/blender.ts
20474
20614
  import fs23 from "fs/promises";
20475
20615
  import path22 from "path";
20476
- var SUBS2 = ["demo", "exec", "snap", "scene", "export", "reset", "mcp", "serve"];
20616
+ var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve"];
20477
20617
  var DEFAULT_OUT_DIR = "assets/blender";
20478
20618
  async function writeB64(dir, name, b64) {
20479
20619
  await fs23.mkdir(dir, { recursive: true });
@@ -20504,7 +20644,7 @@ async function runBlender(opts) {
20504
20644
  return serveLocalBlender({ port, log });
20505
20645
  }
20506
20646
  if (sub === "mcp") {
20507
- const { runBlenderMcp } = await import("./blender-mcp-LLN4FPA4.js");
20647
+ const { runBlenderMcp } = await import("./blender-mcp-3U5AG7WS.js");
20508
20648
  return runBlenderMcp();
20509
20649
  }
20510
20650
  let base = blenderEndpoint();
@@ -20519,8 +20659,8 @@ async function runBlender(opts) {
20519
20659
  return 1;
20520
20660
  }
20521
20661
  if (!base) {
20522
- const [first, ...rest] = BLENDER_SETUP_HINT.split("\n");
20523
- log.error(first ?? "GENEX_BLENDER_URL is not set");
20662
+ const [first, ...rest] = blenderSetupHint().split("\n");
20663
+ log.error(first ?? "No Blender endpoint");
20524
20664
  log.plain(rest.join("\n"));
20525
20665
  return 1;
20526
20666
  }
@@ -20566,12 +20706,31 @@ async function runBlender(opts) {
20566
20706
  case "export": {
20567
20707
  const target = opts.out ?? path22.join(outDir, "scene.glb");
20568
20708
  const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
20709
+ if (!r.glbBase64) {
20710
+ log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
20711
+ return 1;
20712
+ }
20569
20713
  await fs23.mkdir(path22.dirname(target), { recursive: true });
20570
- await fs23.writeFile(target, Buffer.from(r.glbBase64 ?? "", "base64"));
20714
+ await fs23.writeFile(target, Buffer.from(r.glbBase64, "base64"));
20571
20715
  log.success(`Exported ${r.bytes ?? 0} bytes`);
20572
20716
  log.plain(` ${c.cyan(target)}`);
20573
20717
  return 0;
20574
20718
  }
20719
+ case "import": {
20720
+ const url = opts.importUrl?.trim();
20721
+ if (!url || !/^https?:\/\//.test(url)) {
20722
+ log.error("genex blender import needs a GLB URL: genex blender import https://\u2026/model-glb");
20723
+ return 1;
20724
+ }
20725
+ const r = await blenderCall(base, "/import", { url });
20726
+ log.success(`imported ${r.count ?? 0}: ${(r.imported ?? []).join(", ") || "(nothing)"}`);
20727
+ const snap = await blenderCall(base, "/render", { ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
20728
+ const sheet = sheetOf(snap);
20729
+ if (sheet) log.plain(` ${c.cyan(await writeB64(outDir, `contact-sheet.${sheetExt(sheet.mime)}`, sheet.b64))}`);
20730
+ const info = await blenderCall(base, "/scene");
20731
+ reportScene(log, info);
20732
+ return 0;
20733
+ }
20575
20734
  case "exec":
20576
20735
  case "demo": {
20577
20736
  let script;
@@ -21594,6 +21753,13 @@ ${c.bold("Usage")}
21594
21753
  signature move. --lean keeps movement to forward
21595
21754
  walk + run (enemies follow a path).
21596
21755
  genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
21756
+ genex blender <verb> Compose a level or interior in a live Blender \u2014
21757
+ scene | exec --in <file.py> | snap [--mode \u2026] |
21758
+ import <glb-url> | export --out <file.glb> | reset.
21759
+ Hosted sessions get a GPU seat on first use; at a
21760
+ terminal run 'genex blender serve' against your
21761
+ own Blender. Every scene change writes
21762
+ ./assets/blender/contact-sheet.webp \u2014 look at it.
21597
21763
  genex wait <id> Attach to a generation enqueued with --no-wait and
21598
21764
  print its asset URL(s) when it finishes. Safe to
21599
21765
  re-run \u2014 it never creates a new generation.
@@ -22290,6 +22456,13 @@ function parseArgs(argv) {
22290
22456
  parsed.error = `Unexpected argument: ${arg}`;
22291
22457
  return parsed;
22292
22458
  }
22459
+ } else if (parsed.command === "blender") {
22460
+ if (parsed.options.name === "import" && parsed.options.importUrl === void 0) {
22461
+ parsed.options.importUrl = arg;
22462
+ } else {
22463
+ parsed.error = `Unexpected argument: ${arg}`;
22464
+ return parsed;
22465
+ }
22293
22466
  } else {
22294
22467
  parsed.error = `Unexpected argument: ${arg}`;
22295
22468
  return parsed;
@@ -22810,9 +22983,8 @@ async function main() {
22810
22983
  case "ui":
22811
22984
  await runUi({ ...parsed.options, sub: parsed.options.name });
22812
22985
  break;
22813
- // HIDDEN from --help: the M1 test surface for the Blender service spike
22814
- // (apps/blender-service). Gated on GENEX_BLENDER_URL, which no hosted
22815
- // stand sets, so this is a local-only verb until that service is real.
22986
+ // The Blender surface (apps/blender-service): a hosted seat, an explicit
22987
+ // GENEX_BLENDER_URL, or `blender serve` see commands/blender.ts.
22816
22988
  case "blender":
22817
22989
  process.exitCode = await runBlender({ ...parsed.options, sub: parsed.options.name });
22818
22990
  break;