@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/{blender-mcp-LLN4FPA4.js → blender-mcp-3U5AG7WS.js} +3 -3
- package/dist/{chunk-WXHTNSFB.js → chunk-2UJK5OCB.js} +17 -9
- package/dist/index.js +327 -155
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +11 -7
- package/templates/skills/genex-blender-scene/SKILL.md +222 -0
- package/templates/skills/genex-game-director/SKILL.md +81 -38
- package/templates/skills/genex-game-director/references/design-contract.md +13 -5
- package/templates/skills/genex-game-director/references/routing-map.md +16 -45
- package/templates/skills/genex-threejs-procedural-assets/SKILL.md +17 -10
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-
|
|
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
|
|
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.
|
|
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
|
|
2684
|
+
import fs15 from "fs/promises";
|
|
2471
2685
|
import os8 from "os";
|
|
2472
|
-
import
|
|
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
|
|
2737
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
3315
|
-
await
|
|
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
|
|
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 =
|
|
3831
|
-
const maskPath =
|
|
3832
|
-
const annotatedPath =
|
|
3833
|
-
const overlayPath =
|
|
3834
|
-
const metaPath =
|
|
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
|
|
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 =
|
|
3898
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
4571
|
+
const absAssets = path13.resolve(cwd, assetDir);
|
|
4358
4572
|
try {
|
|
4359
|
-
if (!(await
|
|
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(
|
|
4366
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
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
|
|
4370
|
-
const ext =
|
|
4371
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
4450
|
-
mask = PNG2.sync.read(await
|
|
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
|
|
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(
|
|
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 =
|
|
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: `${
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
4974
|
+
const distDir = path14.join(cwd, "dist");
|
|
4760
4975
|
const siteDir = await isDir(distDir) ? distDir : cwd;
|
|
4761
|
-
const rel =
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
5122
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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-
|
|
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] =
|
|
20523
|
-
log.error(first ?? "
|
|
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
|
|
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
|
-
//
|
|
22814
|
-
//
|
|
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;
|