@genex-ai/cli-demo 1.9.2-dev.516 → 1.9.3-dev.518
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
|
@@ -5859,6 +5859,13 @@ async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
|
5859
5859
|
async function countFailed(kind, cwd = process.cwd()) {
|
|
5860
5860
|
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
5861
5861
|
}
|
|
5862
|
+
async function countOutcomes(kind, cwd = process.cwd()) {
|
|
5863
|
+
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
5864
|
+
return {
|
|
5865
|
+
failed: rows.filter((e) => e.status === "failed").length,
|
|
5866
|
+
completed: rows.filter((e) => e.status === "completed").length
|
|
5867
|
+
};
|
|
5868
|
+
}
|
|
5862
5869
|
|
|
5863
5870
|
// src/lib/detect-features.ts
|
|
5864
5871
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
@@ -5986,7 +5993,8 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
5986
5993
|
guessedRepeat: [],
|
|
5987
5994
|
squarePoints: [],
|
|
5988
5995
|
depthRange: [],
|
|
5989
|
-
mediaElementAudio: []
|
|
5996
|
+
mediaElementAudio: [],
|
|
5997
|
+
deferredAudioContext: []
|
|
5990
5998
|
};
|
|
5991
5999
|
const srcDir = path18.join(cwd, "src");
|
|
5992
6000
|
let entries;
|
|
@@ -6046,6 +6054,26 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
6046
6054
|
detail: isCtor ? "new Audio(...)" : 'document.createElement("audio")'
|
|
6047
6055
|
});
|
|
6048
6056
|
}
|
|
6057
|
+
const ctxRe = /([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*=\s*new\s+(?:\(\s*(?:window\s*\.\s*)?(?:webkit)?AudioContext[^)]*\)|(?:window\s*\.\s*)?(?:webkit)?AudioContext)\s*\(/g;
|
|
6058
|
+
while (m = ctxRe.exec(content)) {
|
|
6059
|
+
const target = m[1].replace(/\s+/g, "");
|
|
6060
|
+
const leaf = target.split(".").pop();
|
|
6061
|
+
const esc = leaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6062
|
+
const startsNull = new RegExp(
|
|
6063
|
+
`\\b${esc}\\s*(?::[^;=
|
|
6064
|
+
]*\\|\\s*null[^;=
|
|
6065
|
+
]*)?\\s*=\\s*null\\b|\\b${esc}\\s*:[^;=
|
|
6066
|
+
]*\\|\\s*null`
|
|
6067
|
+
).test(content);
|
|
6068
|
+
const guarded = new RegExp(
|
|
6069
|
+
`!\\s*(?:this\\s*\\.\\s*)?${esc}\\b|\\b${esc}\\s*===?\\s*null\\b`
|
|
6070
|
+
).test(content);
|
|
6071
|
+
if (!startsNull || !guarded) continue;
|
|
6072
|
+
found.deferredAudioContext.push({
|
|
6073
|
+
where: `${rel}:${lineOf(content, m.index)}`,
|
|
6074
|
+
detail: `${target} starts null and is guarded elsewhere in this file`
|
|
6075
|
+
});
|
|
6076
|
+
}
|
|
6049
6077
|
}
|
|
6050
6078
|
return found;
|
|
6051
6079
|
}
|
|
@@ -6054,7 +6082,9 @@ var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
|
6054
6082
|
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
6055
6083
|
const ledger = await readLedger(cwd);
|
|
6056
6084
|
const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
|
|
6057
|
-
if (audited.length === 0)
|
|
6085
|
+
if (audited.length === 0) {
|
|
6086
|
+
return { unwired: [], unpicked: [], unresolved: [], deadWired: [], musicNoSlider: false };
|
|
6087
|
+
}
|
|
6058
6088
|
let haystack = "";
|
|
6059
6089
|
const read = async (file) => {
|
|
6060
6090
|
try {
|
|
@@ -6101,7 +6131,10 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
6101
6131
|
// AFTER the status check converges the ledger to "failed" (otherwise the
|
|
6102
6132
|
// audit self-erases the moment the agent runs the recommended command and
|
|
6103
6133
|
// the dead URL ships silently anyway).
|
|
6104
|
-
deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref)
|
|
6134
|
+
deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref),
|
|
6135
|
+
// One regex covers the HTML/JSX attribute and the DOM-built input alike,
|
|
6136
|
+
// since `.type = "range"` contains `type = "range"`.
|
|
6137
|
+
musicNoSlider: audited.some((e) => e.kind === "music" && e.status === "completed") && !/type\s*=\s*["'`]range["'`]/.test(haystack)
|
|
6105
6138
|
};
|
|
6106
6139
|
}
|
|
6107
6140
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
@@ -6156,6 +6189,11 @@ function generationNudges(log, g) {
|
|
|
6156
6189
|
`${g.unwired.length} finished generation(s) are wired nowhere in the game: ${describe(g.unwired)}. Each was paid for \u2014 wire it in (its URL embeds the id) or tell the user in one plain line why it stays unused.`
|
|
6157
6190
|
);
|
|
6158
6191
|
}
|
|
6192
|
+
if (g.musicNoSlider) {
|
|
6193
|
+
log.warn(
|
|
6194
|
+
`This game paid for a music track and has no volume control anywhere in its source. Music with no Music/SFX sliders is the failure genex-ai-music names: two sliders in the settings screen, persisted and applied live (music ~0.30, sfx ~0.7). A player who cannot turn the music down mutes the browser tab instead, which takes every other sound in the game with it.`
|
|
6195
|
+
);
|
|
6196
|
+
}
|
|
6159
6197
|
}
|
|
6160
6198
|
function surfaceNudges(log, s) {
|
|
6161
6199
|
if (s.guessedRepeat.length) {
|
|
@@ -6176,6 +6214,12 @@ function surfaceNudges(log, s) {
|
|
|
6176
6214
|
`HTML <audio> elements are carrying this game's sound: ${list}. On iPhone an <audio> element is a handle on the operating system's media pipeline in ANOTHER PROCESS: every write to .volume or .playbackRate is a cross-process call, and play() is slow and asynchronous. A sound driven from a live value \u2014 wheels from speed, engine from RPM \u2014 writes 60-120 of those a second and stutters the whole game, while one-shots arrive late. It disguises itself as a graphics problem perfectly: only while moving, identical at every quality setting, desktop fine. THREE.Audio/PositionalAudio on one AudioListener have none of this \u2014 genex-ai-sfx ships the pooled one-shot, the live-value loop, and the decode-during-the-menu preload.`
|
|
6177
6215
|
);
|
|
6178
6216
|
}
|
|
6217
|
+
if (s.deferredAudioContext.length) {
|
|
6218
|
+
const list = s.deferredAudioContext.map((d) => `${d.where} \u2014 ${d.detail}`).join("; ");
|
|
6219
|
+
log.warn(
|
|
6220
|
+
`Sound selected before the AudioContext exists is LOST, not queued: ${list}. Building the context inside the first user gesture is correct \u2014 browsers require it \u2014 but the code around it already skips its work while the context is null, and that is the half that goes wrong: a track chosen on a title screen sets the "current track" variable, its play call is skipped, and when the context finally exists the selector already matches, so nothing ever re-fires. The game then runs with permanently silent music while its SFX (wired directly inside the start handler) play fine. Whatever was pending at the moment the context is created must be RE-KICKED right there. If this game already re-kicks, there is nothing to do \u2014 source alone cannot tell the two apart.`
|
|
6221
|
+
);
|
|
6222
|
+
}
|
|
6179
6223
|
for (const d of s.depthRange) {
|
|
6180
6224
|
log.warn(
|
|
6181
6225
|
`Camera depth range ${d.detail} (far/near = ${d.ratio.toExponential(1)}) at ${d.where} is past what a 24-bit depth buffer can serve \u2014 touching surfaces will z-fight and flicker, and it reads fine right up until it doesn't. near is the expensive knob (resolvable depth \u2248 z\xB2 / (near \xD7 2^24)), so raise near to the closest the camera can actually get \u2014 the quality kit's cameraNearFar derives the pair from the world, and spreading depthRendererOptions() into the renderer adds reversed-Z (free precision at distance, works on WebGL and WebGPU alike): genex-threejs-adaptive-quality. If the range is genuinely astronomical, that is what logarithmicDepthBuffer is for (it costs early-Z): genex-threejs-camera-direction.`
|
|
@@ -7259,6 +7303,9 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
|
7259
7303
|
if (kind === "video") {
|
|
7260
7304
|
const failures = await countFailed("video");
|
|
7261
7305
|
(failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
|
|
7306
|
+
} else {
|
|
7307
|
+
const advice = laneFailureAdvice(kind, await countOutcomes(kind));
|
|
7308
|
+
if (advice) log.plain(advice);
|
|
7262
7309
|
}
|
|
7263
7310
|
}
|
|
7264
7311
|
process.exitCode = 1;
|
|
@@ -7317,6 +7364,10 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
|
7317
7364
|
}
|
|
7318
7365
|
printHint(kind, view, files, log);
|
|
7319
7366
|
}
|
|
7367
|
+
function laneFailureAdvice(kind, outcomes) {
|
|
7368
|
+
if (outcomes.failed < 2 || outcomes.completed > 0) return null;
|
|
7369
|
+
return ` ${c.bold(`\u270B ${outcomes.failed} ${kind} generations attempted in this project, ${outcomes.failed} failed, none succeeded.`)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
|
|
7370
|
+
}
|
|
7320
7371
|
function videoFailureAdvice(failures) {
|
|
7321
7372
|
if (failures >= 2) {
|
|
7322
7373
|
return ` ${c.bold(`\u270B ${failures} failed videos in this project \u2014 don't queue another attempt of the same clip.`)} One retry per clip is the budget. Use its key-art still as the backdrop instead (the genex-ai-menu skill's built-in fallback; a slow CSS pan gives it life), tell the user in one plain line, and spend the minutes in the game.`;
|
|
@@ -7436,31 +7487,50 @@ async function reportTextureSeam(url, log) {
|
|
|
7436
7487
|
} catch {
|
|
7437
7488
|
}
|
|
7438
7489
|
}
|
|
7439
|
-
|
|
7440
|
-
|
|
7490
|
+
var HINT_URL_KINDS = /* @__PURE__ */ new Set([
|
|
7491
|
+
"model",
|
|
7492
|
+
"skybox",
|
|
7493
|
+
"sfx",
|
|
7494
|
+
"music",
|
|
7495
|
+
"voice",
|
|
7496
|
+
"image",
|
|
7497
|
+
"video",
|
|
7498
|
+
"model_segment",
|
|
7499
|
+
"model_rig",
|
|
7500
|
+
"model_animation"
|
|
7501
|
+
]);
|
|
7502
|
+
function assetHint(kind, view, url) {
|
|
7441
7503
|
const hint = {
|
|
7442
|
-
model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill
|
|
7504
|
+
model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill.`,
|
|
7443
7505
|
// Unreachable while the lane is paused (see src/index.ts) — kept so a
|
|
7444
7506
|
// restore is one deletion. Games that already ship a panorama still load
|
|
7445
7507
|
// it this way.
|
|
7446
|
-
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment
|
|
7447
|
-
|
|
7448
|
-
music
|
|
7449
|
-
|
|
7508
|
+
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment.`,
|
|
7509
|
+
// The boot-order clause is not decoration: a hosted run shipped three paid
|
|
7510
|
+
// music tracks and a silent title screen because the game selected a track
|
|
7511
|
+
// while its AudioContext was still null. NAME the failure — pointing at the
|
|
7512
|
+
// skill does not work, because no skill states this rule.
|
|
7513
|
+
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener). Create the AudioContext inside the first user gesture \u2014 anything you fire before it exists is LOST, not queued \u2014 see genex-ai-sfx.`,
|
|
7514
|
+
music: `Load with AudioLoader into a non-positional THREE.Audio: setLoop(true), setVolume(0.30). Create the AudioContext inside the first user gesture, and RE-KICK whatever track was already selected: a track chosen while the context was null is LOST, not queued, so nothing plays until the selection happens to change again. The settings screen MUST have Music + SFX volume sliders \u2014 see genex-ai-music.`,
|
|
7515
|
+
voice: `Load with AudioLoader into a THREE.Audio (or PositionalAudio at the speaker); voice lines are content \u2014 keep them short, SUBTITLED, and skippable \u2014 see genex-ai-voice.`,
|
|
7450
7516
|
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
|
|
7451
|
-
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image
|
|
7452
|
-
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video
|
|
7517
|
+
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image.`,
|
|
7518
|
+
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video.`,
|
|
7453
7519
|
motion: `Raw ARDY takes + verify report \u2014 download the takes, rename them meaningfully (routing is by filename), then: genex motion verify \u2192 compile \u2192 install. Full procedure: the genex-ai-character skill's motion reference.`,
|
|
7454
7520
|
character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
|
|
7455
7521
|
character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
|
|
7456
7522
|
character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
|
|
7457
7523
|
character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
|
|
7458
7524
|
character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`,
|
|
7459
|
-
model_segment: `One GLB with NAMED parts \u2014 load with GLTFLoader, then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them
|
|
7460
|
-
model_rig: `Rigged GLB (Tripo skeleton) \u2014 load with GLTFLoader; give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code
|
|
7461
|
-
model_animation: `Animated GLB \u2014 load with GLTFLoader and play its clips via THREE.AnimationMixer (gltf.animations)
|
|
7525
|
+
model_segment: `One GLB with NAMED parts \u2014 load with GLTFLoader, then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
|
|
7526
|
+
model_rig: `Rigged GLB (Tripo skeleton) \u2014 load with GLTFLoader; give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
|
|
7527
|
+
model_animation: `Animated GLB \u2014 load with GLTFLoader and play its clips via THREE.AnimationMixer (gltf.animations).`
|
|
7462
7528
|
};
|
|
7463
|
-
|
|
7529
|
+
const base = hint[kind];
|
|
7530
|
+
return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
|
|
7531
|
+
}
|
|
7532
|
+
function printHint(kind, view, files, log) {
|
|
7533
|
+
log.dim(` ${assetHint(kind, view, files[0]?.url ?? "")}`);
|
|
7464
7534
|
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
7465
7535
|
}
|
|
7466
7536
|
var CONCEPT_STOP = "STOP \u2014 show all three concept images to the user and wait for an explicit selection. Do not create the 3D preview until they choose candidate 1, 2, or 3.";
|
|
@@ -7826,8 +7896,11 @@ async function runWaitAll(opts) {
|
|
|
7826
7896
|
}
|
|
7827
7897
|
}
|
|
7828
7898
|
const rows = [];
|
|
7899
|
+
const landed = [];
|
|
7829
7900
|
for (const e of ledger) {
|
|
7830
|
-
|
|
7901
|
+
const v = views.get(e.id);
|
|
7902
|
+
if (e.status === "queued" && v?.status === "completed") landed.push({ kind: e.kind, view: v });
|
|
7903
|
+
rows.push(await toRow(e, v, cwd));
|
|
7831
7904
|
}
|
|
7832
7905
|
if (opts.json) {
|
|
7833
7906
|
writeJson({ generations: rows });
|
|
@@ -7854,6 +7927,14 @@ async function runWaitAll(opts) {
|
|
|
7854
7927
|
if (count("running") + count("queued") > 0) {
|
|
7855
7928
|
log.dim(` Attach to any single one with: ${c.cyan("npx genex wait <id>")}`);
|
|
7856
7929
|
}
|
|
7930
|
+
if (landed.length > 0) {
|
|
7931
|
+
log.plain("");
|
|
7932
|
+
log.plain(` ${c.bold("Just landed")} \u2014 how to use each one:`);
|
|
7933
|
+
for (const l of landed) {
|
|
7934
|
+
const hint = assetHint(l.kind, l.view);
|
|
7935
|
+
if (hint) log.dim(` \u2022 ${l.kind}: ${hint}`);
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7857
7938
|
log.plain("");
|
|
7858
7939
|
log.dim(
|
|
7859
7940
|
" \u{1F4DC} Resumed or compacted? Re-read AGENTS.md + DESIGN.md (continue from its Now: line) before building on."
|
|
@@ -21567,10 +21648,11 @@ async function main() {
|
|
|
21567
21648
|
await runCharacterAnimateMotion({ ...parsed.options, creature: true });
|
|
21568
21649
|
break;
|
|
21569
21650
|
}
|
|
21570
|
-
if (!parsed.options.
|
|
21571
|
-
|
|
21572
|
-
|
|
21573
|
-
|
|
21651
|
+
if (!parsed.options.quiet) {
|
|
21652
|
+
const note = " Biped-shaped creatures only (Meshy rig limit) \u2014 for a true quadruped/flier/serpent, generate it with `npx genex model` and rig it with `npx genex model rig` (7 body plans), or go static + the genex-threejs-creatures procedural recipes.";
|
|
21653
|
+
if (parsed.options.json) process.stderr.write(`${note}
|
|
21654
|
+
`);
|
|
21655
|
+
else log.dim(note);
|
|
21574
21656
|
}
|
|
21575
21657
|
await runCharacter({
|
|
21576
21658
|
...parsed.options,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3-dev.518",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -190,8 +190,10 @@ Search the catalog first. A missing verb uses
|
|
|
190
190
|
`genex character animate <id> "<verb>"` or the creature equivalent, with the
|
|
191
191
|
existing free motion plan before paid work, route selection, source/clip
|
|
192
192
|
validation, locomotion bindings, manifests, controller authority, and runtime
|
|
193
|
-
playback unchanged.
|
|
194
|
-
|
|
193
|
+
playback unchanged. A non-biped BODY now has a rigged route —
|
|
194
|
+
`npx genex model` → `model rig --type <plan>` → `model animate` (7 body plans,
|
|
195
|
+
AG-908) — so `$genex-threejs-procedural-animation` is the fallback for shapes
|
|
196
|
+
with no riggable body plan, and it keeps prop/debris motion outright.
|
|
195
197
|
|
|
196
198
|
### Optimization and mobile
|
|
197
199
|
|