@genex-ai/cli-demo 0.61.0-dev.141 → 0.62.0-dev.143

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
@@ -2755,602 +2755,8 @@ async function runWait(opts) {
2755
2755
  import fs12 from "fs/promises";
2756
2756
  import path13 from "path";
2757
2757
 
2758
- // src/lib/anims.ts
2759
- import fs11 from "fs/promises";
2760
- import path12 from "path";
2761
- var ANIMS_DEST = path12.join("public", "assets", "anims");
2762
- var HIDDEN_TAG = "reference";
2763
- async function runAnims(opts) {
2764
- const log = createLogger({ quiet: opts.quiet });
2765
- const root = opts.cwd ?? process.cwd();
2766
- const selectors = opts.selectors ?? [];
2767
- log.plain(c.bold("genex controller anims"));
2768
- log.plain("");
2769
- const { manifest, source } = await loadManifest(opts.animsBase);
2770
- if (source === "snapshot") {
2771
- log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
2772
- }
2773
- if (opts.list) {
2774
- printCatalog(log, manifest, selectors);
2775
- return;
2776
- }
2777
- const controllerMarker = path12.join(root, "src", "controllers", "character");
2778
- if (!await exists2(controllerMarker)) {
2779
- log.error(
2780
- `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
2781
- );
2782
- log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
2783
- process.exitCode = 1;
2784
- return;
2785
- }
2786
- const destDir = path12.join(root, ANIMS_DEST);
2787
- const gameManifestPath = path12.join(destDir, "manifest.json");
2788
- if (opts.reset) {
2789
- await fs11.rm(destDir, { recursive: true, force: true });
2790
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
2791
- }
2792
- if (selectors.length === 0) {
2793
- const installed = await readGameManifest(gameManifestPath);
2794
- if (installed === null || installed.clips.length === 0) {
2795
- log.plain(" No animation packs installed yet.");
2796
- } else {
2797
- log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
2798
- }
2799
- log.plain("");
2800
- log.plain(
2801
- ` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
2802
- "genex controller anims --list"
2803
- )}.`
2804
- );
2805
- return;
2806
- }
2807
- let resolved;
2808
- try {
2809
- resolved = resolveSelectors(manifest, selectors);
2810
- } catch (err) {
2811
- log.error(err instanceof Error ? err.message : String(err));
2812
- process.exitCode = 1;
2813
- return;
2814
- }
2815
- const coreNames = new Set(manifest.core);
2816
- const byName = /* @__PURE__ */ new Map();
2817
- let bundledSkips = 0;
2818
- for (const entries of resolved.values()) {
2819
- for (const entry of entries) {
2820
- if (coreNames.has(entry.name)) {
2821
- bundledSkips++;
2822
- continue;
2823
- }
2824
- byName.set(entry.name, entry);
2825
- }
2826
- }
2827
- const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2828
- const cacheDir = path12.join(
2829
- opts.cacheDir ?? getAnimsCacheDir(),
2830
- `${manifest.library}-v${manifest.version}`
2831
- );
2832
- await fs11.mkdir(cacheDir, { recursive: true });
2833
- await fs11.mkdir(destDir, { recursive: true });
2834
- const base = getAnimsBase(opts.animsBase);
2835
- let installedCount = 0;
2836
- let presentCount = 0;
2837
- let addedBytes = 0;
2838
- const failures = [];
2839
- for (const entry of wanted) {
2840
- const dest = path12.join(destDir, entry.file);
2841
- if (await hasSize(dest, entry.bytes)) {
2842
- presentCount++;
2843
- continue;
2844
- }
2845
- try {
2846
- const cached = path12.join(cacheDir, entry.file);
2847
- if (!await hasSize(cached, entry.bytes)) {
2848
- const res = await fetch(base + entry.file);
2849
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
2850
- const buf = Buffer.from(await res.arrayBuffer());
2851
- await fs11.writeFile(cached, buf);
2852
- }
2853
- await fs11.copyFile(cached, dest);
2854
- installedCount++;
2855
- addedBytes += entry.bytes;
2856
- log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
2857
- } catch (err) {
2858
- failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
2859
- }
2860
- }
2861
- const previous = await readGameManifest(gameManifestPath);
2862
- const union = new Set(previous?.clips ?? []);
2863
- for (const entry of wanted) {
2864
- if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
2865
- }
2866
- const gameManifest = {
2867
- schema: 1,
2868
- library: manifest.library,
2869
- version: manifest.version,
2870
- clips: [...union].sort((a, b) => a.localeCompare(b))
2871
- };
2872
- await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
2873
- log.plain("");
2874
- const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
2875
- if (presentCount > 0) parts.push(`${presentCount} already present`);
2876
- if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
2877
- log.success(
2878
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
2879
- );
2880
- for (const [selector, entries] of resolved) {
2881
- const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
2882
- if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
2883
- }
2884
- log.info(
2885
- `Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
2886
- );
2887
- if (failures.length > 0) {
2888
- log.plain("");
2889
- log.error(
2890
- `${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
2891
- );
2892
- log.plain(
2893
- " Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
2894
- );
2895
- process.exitCode = 1;
2896
- }
2897
- }
2898
- async function loadManifest(baseOverride) {
2899
- const base = getAnimsBase(baseOverride);
2900
- try {
2901
- const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
2902
- if (res.ok) {
2903
- const manifest2 = await res.json();
2904
- if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
2905
- return { manifest: manifest2, source: "cdn" };
2906
- }
2907
- }
2908
- } catch {
2909
- }
2910
- const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
2911
- const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
2912
- return { manifest, source: "snapshot" };
2913
- }
2914
- function resolveSelectors(manifest, selectors) {
2915
- const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
2916
- const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
2917
- const tags = /* @__PURE__ */ new Map();
2918
- for (const entry of manifest.clips) {
2919
- for (const tag of entry.tags) {
2920
- const list = tags.get(tag) ?? [];
2921
- list.push(entry);
2922
- tags.set(tag, list);
2923
- }
2924
- }
2925
- const out = /* @__PURE__ */ new Map();
2926
- for (const selector of selectors) {
2927
- const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
2928
- if (exact) {
2929
- out.set(selector, [exact]);
2930
- continue;
2931
- }
2932
- const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
2933
- if (tagHit) {
2934
- out.set(selector, tagHit);
2935
- continue;
2936
- }
2937
- const candidates = [...tags.keys(), ...byName.keys()];
2938
- const close = suggest(selector, candidates);
2939
- throw new Error(
2940
- `unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
2941
- "genex controller anims --list"
2942
- )} for the catalog.`
2943
- );
2944
- }
2945
- return out;
2946
- }
2947
- function suggest(input, candidates) {
2948
- const lower = input.toLowerCase();
2949
- const scored = [];
2950
- for (const candidate of candidates) {
2951
- const candidateLower = candidate.toLowerCase();
2952
- if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
2953
- scored.push({ name: candidate, score: 0 });
2954
- continue;
2955
- }
2956
- const distance = levenshtein(lower, candidateLower, 2);
2957
- if (distance <= 2) scored.push({ name: candidate, score: distance });
2958
- }
2959
- scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
2960
- return scored.slice(0, 4).map((s) => s.name);
2961
- }
2962
- function levenshtein(a, b, max) {
2963
- if (Math.abs(a.length - b.length) > max) return max + 1;
2964
- let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
2965
- for (let i = 1; i <= a.length; i++) {
2966
- const curr = [i];
2967
- let rowMin = i;
2968
- for (let j = 1; j <= b.length; j++) {
2969
- curr[j] = Math.min(
2970
- prev[j] + 1,
2971
- curr[j - 1] + 1,
2972
- prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2973
- );
2974
- if (curr[j] < rowMin) rowMin = curr[j];
2975
- }
2976
- if (rowMin > max) return max + 1;
2977
- prev = curr;
2978
- }
2979
- return prev[b.length];
2980
- }
2981
- function printCatalog(log, manifest, selectors) {
2982
- const coreNames = new Set(manifest.core);
2983
- if (selectors.length > 0) {
2984
- let resolved;
2985
- try {
2986
- resolved = resolveSelectors(manifest, selectors);
2987
- } catch (err) {
2988
- log.error(err instanceof Error ? err.message : String(err));
2989
- process.exitCode = 1;
2990
- return;
2991
- }
2992
- for (const [selector, entries] of resolved) {
2993
- log.plain(c.bold(selector));
2994
- for (const entry of entries) {
2995
- const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
2996
- log.plain(
2997
- ` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
2998
- );
2999
- }
3000
- }
3001
- return;
3002
- }
3003
- const tags = /* @__PURE__ */ new Map();
3004
- for (const entry of manifest.clips) {
3005
- for (const tag of entry.tags) {
3006
- if (tag === HIDDEN_TAG) continue;
3007
- const list = tags.get(tag) ?? [];
3008
- list.push(entry);
3009
- tags.set(tag, list);
3010
- }
3011
- }
3012
- log.plain(
3013
- `${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
3014
- );
3015
- log.plain(
3016
- ` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
3017
- "genex controller anims --list <tag>"
3018
- )}`
3019
- );
3020
- log.plain("");
3021
- for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
3022
- const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
3023
- const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
3024
- log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
3025
- }
3026
- }
3027
- async function readGameManifest(file) {
3028
- try {
3029
- return JSON.parse(await fs11.readFile(file, "utf8"));
3030
- } catch {
3031
- return null;
3032
- }
3033
- }
3034
- async function hasSize(file, bytes) {
3035
- try {
3036
- return (await fs11.stat(file)).size === bytes;
3037
- } catch {
3038
- return false;
3039
- }
3040
- }
3041
- async function exists2(p) {
3042
- try {
3043
- await fs11.access(p);
3044
- return true;
3045
- } catch {
3046
- return false;
3047
- }
3048
- }
3049
- function formatMb(bytes) {
3050
- return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
3051
- }
3052
-
3053
- // src/commands/controller.ts
3054
- var CONTROLLER_KINDS = [
3055
- "character",
3056
- "car",
3057
- "drone",
3058
- "touch",
3059
- "networked-physics"
3060
- ];
3061
- var SHARED = [
3062
- "shared/math.ts",
3063
- "shared/physics-world.ts",
3064
- "shared/colliders.ts"
3065
- ];
3066
- var TOUCH_KIT = [
3067
- "touch/touch-joystick.ts",
3068
- "touch/drag-zone.ts",
3069
- "touch/rotate-overlay.ts"
3070
- ];
3071
- var INPUT_AND_CAMERA = [
3072
- "character/follow-camera.ts",
3073
- "character/keyboard-input.ts",
3074
- "character/touch-joystick.ts",
3075
- ...TOUCH_KIT
3076
- ];
3077
- var NOTICE = "NOTICE.md";
3078
- var CONTROLLER_FILE_SETS = {
3079
- character: {
3080
- code: [
3081
- ...SHARED,
3082
- "character/character-controller.ts",
3083
- "character/character-animations.ts",
3084
- "character/animation-packs.ts",
3085
- "character/motion-actions.ts",
3086
- "character/meshy/meshy-loader.ts",
3087
- "character/presets.ts",
3088
- // VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
3089
- // the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
3090
- "character/vrm/vrm-loader.ts",
3091
- "character/vrm/vrm-retarget.ts",
3092
- "character/vrm/capsule-fit.ts",
3093
- "character/vrm/foot-ik.ts",
3094
- ...INPUT_AND_CAMERA,
3095
- NOTICE
3096
- ],
3097
- // The player's VRM is written to public/assets/avatar.vrm at install time by
3098
- // installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
3099
- // a static manifest asset. animation-library.glb (the 12-clip core) still is;
3100
- // extra clips arrive via `genex controller anims` into public/assets/anims/.
3101
- assets: ["assets/animation-library.glb"],
3102
- skill: "genex-threejs-character-controller",
3103
- sketch: [
3104
- `const physics = await PhysicsWorld.create();`,
3105
- `const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
3106
- `const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
3107
- `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
3108
- `character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
3109
- `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
3110
- ]
3111
- },
3112
- car: {
3113
- code: [
3114
- ...SHARED,
3115
- "vehicle/vehicle-controller.ts",
3116
- "vehicle/wheel.ts",
3117
- "vehicle/presets.ts",
3118
- "interact/enter-exit.ts",
3119
- ...INPUT_AND_CAMERA,
3120
- NOTICE
3121
- ],
3122
- assets: [],
3123
- skill: "genex-threejs-vehicle-controllers",
3124
- sketch: [
3125
- `const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
3126
- `const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
3127
- `scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
3128
- ]
3129
- },
3130
- drone: {
3131
- code: [
3132
- ...SHARED,
3133
- "drone/drone-controller.ts",
3134
- "drone/presets.ts",
3135
- "interact/enter-exit.ts",
3136
- ...INPUT_AND_CAMERA,
3137
- NOTICE
3138
- ],
3139
- assets: [],
3140
- skill: "genex-threejs-vehicle-controllers",
3141
- sketch: [
3142
- `const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
3143
- `const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
3144
- `physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
3145
- ]
3146
- },
3147
- touch: {
3148
- code: [...TOUCH_KIT, NOTICE],
3149
- assets: [],
3150
- skill: "genex-threejs-touch-controls",
3151
- sketch: [
3152
- `const joy = new TouchJoystick({ floating: true }); // safe-area-aware defaults; static circle without the flag`,
3153
- `const jump = new VirtualButton({ label: "Jump", onPress: () => player.jump() });`,
3154
- `const look = new DragZone(); // right half; per frame: const { dx, dy } = look.consumeDelta()`,
3155
- `[joy, jump, look].forEach((w) => w.setVisible(navigator.maxTouchPoints > 0));`
3156
- ]
3157
- },
3158
- "networked-physics": {
3159
- code: [
3160
- ...SHARED,
3161
- "network/pose.ts",
3162
- "network/networked-pushable.ts",
3163
- "network/networked-vehicle.ts",
3164
- "NETWORKING.md",
3165
- NOTICE
3166
- ],
3167
- assets: [],
3168
- skill: "genex-threejs-multiplayer",
3169
- sketch: [
3170
- `const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
3171
- `physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
3172
- `contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
3173
- ]
3174
- }
3175
- };
3176
- var CODE_DEST = path13.join("src", "controllers");
3177
- var ASSETS_DEST = path13.join("public", "assets");
3178
- async function runController(opts) {
3179
- const log = createLogger({ quiet: opts.quiet });
3180
- if (opts.kind?.trim() === "anims") {
3181
- await runAnims(opts);
3182
- return;
3183
- }
3184
- const kind = opts.kind?.trim();
3185
- if (!kind || !CONTROLLER_KINDS.includes(kind)) {
3186
- log.error(
3187
- `Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
3188
- "genex controller <character|car|drone|touch|networked-physics> [--force]"
3189
- )} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
3190
- );
3191
- process.exitCode = 1;
3192
- return;
3193
- }
3194
- const srcDir = path13.join(getTemplatesDir(), "controllers");
3195
- const root = opts.cwd ?? process.cwd();
3196
- const set = CONTROLLER_FILE_SETS[kind];
3197
- log.plain(c.bold(`genex controller ${kind}`));
3198
- log.plain("");
3199
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
3200
- const plan = [
3201
- ...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
3202
- ...set.assets.map((rel) => ({
3203
- from: rel,
3204
- rel: path13.join(ASSETS_DEST, path13.basename(rel))
3205
- }))
3206
- ];
3207
- let copied = 0;
3208
- let skipped = 0;
3209
- try {
3210
- for (const file of plan) {
3211
- const dest = path13.join(root, file.rel);
3212
- if (!opts.force && await exists3(dest)) {
3213
- skipped++;
3214
- log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
3215
- continue;
3216
- }
3217
- await fs12.mkdir(path13.dirname(dest), { recursive: true });
3218
- await fs12.copyFile(path13.join(srcDir, file.from), dest);
3219
- copied++;
3220
- log.dim(` ${file.rel}`);
3221
- }
3222
- } catch (err) {
3223
- log.error(`Copy failed: ${String(err)}`);
3224
- process.exitCode = 1;
3225
- return;
3226
- }
3227
- log.success(
3228
- `Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
3229
- );
3230
- log.plain("");
3231
- if (kind === "character") {
3232
- if (opts.character) {
3233
- try {
3234
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
3235
- if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
3236
- await installMeshyCharacterManifest({
3237
- root,
3238
- characterId: opts.character,
3239
- apiUrl: getApiUrl(opts.apiUrl),
3240
- token,
3241
- log
3242
- });
3243
- } catch (error) {
3244
- log.error(error instanceof Error ? error.message : String(error));
3245
- process.exitCode = 1;
3246
- return;
3247
- }
3248
- } else {
3249
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
3250
- await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
3251
- }
3252
- log.plain("");
3253
- }
3254
- log.plain(c.bold("Next steps"));
3255
- if (kind !== "touch") {
3256
- log.plain(
3257
- ` 1. ${c.cyan(
3258
- kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
3259
- )} (three is already in the scaffold).`
3260
- );
3261
- }
3262
- const stepOffset = kind === "touch" ? 0 : 1;
3263
- log.plain(
3264
- ` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
3265
- );
3266
- log.plain(
3267
- kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
3268
- );
3269
- const sketch = kind === "character" && opts.character ? [
3270
- `const physics = await PhysicsWorld.create();`,
3271
- `const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
3272
- `const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
3273
- `character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
3274
- `physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
3275
- ] : set.sketch;
3276
- for (const line of sketch) {
3277
- log.dim(` ${line}`);
3278
- }
3279
- await firstPreviewNudge(log);
3280
- }
3281
- async function installMeshyCharacterManifest(args) {
3282
- const response = await apiFetch(
3283
- `${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
3284
- { headers: { Authorization: `Bearer ${args.token}` } }
3285
- );
3286
- if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
3287
- if (!response.ok) throw new Error(`Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
3288
- const body = await response.json();
3289
- const manifest = body.manifest;
3290
- if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId) {
3291
- throw new Error("The API returned an invalid Meshy character manifest.");
3292
- }
3293
- const destination = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
3294
- await fs12.mkdir(path13.dirname(destination), { recursive: true });
3295
- await fs12.writeFile(
3296
- destination,
3297
- `${JSON.stringify(manifest, null, 2)}
3298
- `
3299
- );
3300
- args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
3301
- const pack = manifest.controllerPack;
3302
- if (typeof pack?.key === "string" && typeof pack.version === "number") {
3303
- args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
3304
- } else {
3305
- args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the preview-reviewed neutral-v2 locomotion pack.");
3306
- }
3307
- const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
3308
- args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
3309
- const slots = Object.keys(manifest.locomotion?.bindings ?? manifest.locomotion?.slots ?? {}).sort();
3310
- args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
3311
- const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
3312
- if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
3313
- else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
3314
- }
3315
- async function installOwnerAvatar(args) {
3316
- const { root, srcDir, apiUrl, token, log } = args;
3317
- const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
3318
- await fs12.mkdir(path13.dirname(dest), { recursive: true });
3319
- if (token) {
3320
- try {
3321
- const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
3322
- headers: { Authorization: `Bearer ${token}` }
3323
- });
3324
- if (res.ok) {
3325
- const me = await res.json();
3326
- if (me.vrmUrl) {
3327
- const vrmRes = await fetch(me.vrmUrl);
3328
- if (vrmRes.ok) {
3329
- const buf = Buffer.from(await vrmRes.arrayBuffer());
3330
- await fs12.writeFile(dest, buf);
3331
- log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
3332
- return;
3333
- }
3334
- }
3335
- }
3336
- log.dim(" couldn't fetch your avatar; using the bundled default.");
3337
- } catch {
3338
- log.dim(" avatar fetch failed (offline?); using the bundled default.");
3339
- }
3340
- }
3341
- await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
3342
- log.dim(
3343
- token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
3344
- );
3345
- }
3346
- async function exists3(p) {
3347
- try {
3348
- await fs12.access(p);
3349
- return true;
3350
- } catch {
3351
- return false;
3352
- }
3353
- }
2758
+ // ../../packages/meshy-animation-catalog/src/index.ts
2759
+ import { createHash } from "crypto";
3354
2760
 
3355
2761
  // ../../packages/meshy-animation-catalog/src/generated.ts
3356
2762
  var MESHY_ANIMATION_CATALOG = [
@@ -12194,215 +11600,992 @@ var MESHY_ANIMATION_CATALOG = [
12194
11600
  "isFree": false,
12195
11601
  "createdAt": 1750829526124
12196
11602
  }
12197
- ];
12198
-
12199
- // ../../packages/meshy-animation-catalog/src/curation.ts
12200
- var CURATED = {
12201
- [-2]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["walk.forward"] },
12202
- [-1]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["run.forward"] },
12203
- [0]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["idle.default"] },
12204
- [243]: {
12205
- loop: true,
12206
- motionPolicy: "controller-loop",
12207
- controllerSlots: ["idle.default"],
12208
- reviewStatus: "preview-reviewed"
12209
- },
12210
- [466]: {
12211
- loop: false,
12212
- motionPolicy: "anchored-action",
12213
- controllerSlots: ["jump.full"],
12214
- reviewStatus: "preview-reviewed"
12215
- },
12216
- [613]: {
12217
- loop: true,
12218
- motionPolicy: "controller-loop",
12219
- controllerSlots: ["walk.forward"],
12220
- reviewStatus: "preview-reviewed"
12221
- },
12222
- [616]: {
12223
- loop: true,
12224
- motionPolicy: "controller-loop",
12225
- controllerSlots: ["crouch.forward"],
12226
- reviewStatus: "preview-reviewed"
12227
- },
12228
- [657]: {
12229
- loop: true,
12230
- motionPolicy: "controller-loop",
12231
- controllerSlots: ["run.forward"],
12232
- reviewStatus: "preview-reviewed"
12233
- },
12234
- [658]: {
12235
- loop: true,
12236
- motionPolicy: "controller-loop",
12237
- controllerSlots: [],
12238
- reviewStatus: "rejected"
12239
- },
12240
- [659]: {
12241
- loop: true,
12242
- motionPolicy: "controller-loop",
12243
- controllerSlots: [],
12244
- reviewStatus: "rejected"
11603
+ ];
11604
+
11605
+ // ../../packages/meshy-animation-catalog/src/curation.ts
11606
+ var CURATED = {
11607
+ [-2]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["walk.forward"] },
11608
+ [-1]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["run.forward"] },
11609
+ [0]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["idle.default"] },
11610
+ [243]: {
11611
+ loop: true,
11612
+ motionPolicy: "controller-loop",
11613
+ controllerSlots: ["idle.default"],
11614
+ reviewStatus: "preview-reviewed"
11615
+ },
11616
+ [466]: {
11617
+ loop: false,
11618
+ motionPolicy: "anchored-action",
11619
+ controllerSlots: ["jump.full"],
11620
+ reviewStatus: "preview-reviewed"
11621
+ },
11622
+ [613]: {
11623
+ loop: true,
11624
+ motionPolicy: "controller-loop",
11625
+ controllerSlots: ["walk.forward"],
11626
+ reviewStatus: "preview-reviewed"
11627
+ },
11628
+ [616]: {
11629
+ loop: true,
11630
+ motionPolicy: "controller-loop",
11631
+ controllerSlots: ["crouch.forward"],
11632
+ reviewStatus: "preview-reviewed"
11633
+ },
11634
+ [657]: {
11635
+ loop: true,
11636
+ motionPolicy: "controller-loop",
11637
+ controllerSlots: ["run.forward"],
11638
+ reviewStatus: "preview-reviewed"
11639
+ },
11640
+ [658]: {
11641
+ loop: true,
11642
+ motionPolicy: "controller-loop",
11643
+ controllerSlots: [],
11644
+ reviewStatus: "rejected"
11645
+ },
11646
+ [659]: {
11647
+ loop: true,
11648
+ motionPolicy: "controller-loop",
11649
+ controllerSlots: [],
11650
+ reviewStatus: "rejected"
11651
+ }
11652
+ };
11653
+ var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
11654
+ var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
11655
+ "Climbing",
11656
+ "HangingfromLedge",
11657
+ "VaultingOverObstacle",
11658
+ "Interacting",
11659
+ "PickingUpItem",
11660
+ "Pushing",
11661
+ "Sleeping"
11662
+ ]);
11663
+ var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
11664
+ var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
11665
+ function words(value) {
11666
+ return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
11667
+ }
11668
+ function inferRequirements(key, subCategory) {
11669
+ const lower = `${key} ${subCategory}`.toLowerCase().replace(/[^a-z0-9]+/g, " ");
11670
+ const props = [
11671
+ ["gun", /\b(?:gun|rifle|pistol)\b/],
11672
+ ["sword", /\b(?:sword|blade)\b/],
11673
+ ["bow", /\b(?:bow|arrow)\b/],
11674
+ ["shield", /\bshield\b/],
11675
+ ["chair", /\b(?:chair|sit|sitting)\b/],
11676
+ ["ladder", /\bladder\b/],
11677
+ ["rope", /\brope\b/]
11678
+ ].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
11679
+ const environment = [
11680
+ ["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
11681
+ ["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
11682
+ ["water", /\b(?:swim|swimming)\b/],
11683
+ ["seat", /\b(?:sit|sitting|chair)\b/]
11684
+ ].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
11685
+ return { props, environment, partner: /\b(?:partner|carry person|hug|handshake)\b/.test(lower) };
11686
+ }
11687
+ function inferSlots(key) {
11688
+ const lower = key.toLowerCase();
11689
+ const band = lower.includes("crouch") ? "crouch" : lower.includes("run") || lower.includes("sprint") ? "run" : lower.includes("walk") ? "walk" : null;
11690
+ if (!band) return [];
11691
+ const direction = lower.includes("backleft") ? "back-left" : lower.includes("backright") ? "back-right" : lower.includes("backward") || lower.includes("_back") ? "backward" : lower.includes("left") ? "left" : lower.includes("right") ? "right" : "forward";
11692
+ return [`${band}.${direction}`];
11693
+ }
11694
+ function curateMeshyAnimation(raw) {
11695
+ const inPlace = raw.tag === "InPlace";
11696
+ const requirements = inferRequirements(raw.key, raw.subCategory);
11697
+ const choreography = CHOREOGRAPHY_SUBCATEGORIES.has(raw.subCategory) || requirements.partner;
11698
+ const loop = inPlace && LOOP_SUBCATEGORIES.has(raw.subCategory) && !NON_LOOP_ACTION.test(`${raw.key} ${raw.name}`);
11699
+ const motionPolicy = loop ? "controller-loop" : choreography ? "choreography" : PLANAR_ACTION.test(`${raw.key} ${raw.name}`) ? "planar-root-action" : "anchored-action";
11700
+ const base = {
11701
+ actionId: raw.actionId,
11702
+ key: raw.key,
11703
+ name: raw.name,
11704
+ category: raw.category,
11705
+ subCategory: raw.subCategory,
11706
+ previewUrl: raw.previewUrl,
11707
+ rigType: raw.rigType,
11708
+ inPlace,
11709
+ isDefault: raw.isDefault,
11710
+ isFree: raw.isFree,
11711
+ createdAt: raw.createdAt,
11712
+ aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
11713
+ gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
11714
+ loop,
11715
+ motionPolicy,
11716
+ rootMotionValidated: false,
11717
+ // A name that contains "walk" or "run" is not enough to make a safe
11718
+ // controller loop. Only provider-declared InPlace loops (plus explicit
11719
+ // measured overrides above) may populate automatic locomotion slots.
11720
+ controllerSlots: loop ? inferSlots(raw.key) : [],
11721
+ requirements,
11722
+ reviewStatus: "metadata-reviewed"
11723
+ };
11724
+ return { ...base, ...CURATED[raw.actionId], requirements };
11725
+ }
11726
+
11727
+ // ../../packages/meshy-animation-catalog/src/index.ts
11728
+ function normalizedPackDefinition(definition) {
11729
+ const key = definition.key.trim();
11730
+ if (!key) throw new Error("Meshy controller-pack key cannot be empty");
11731
+ if (!Number.isInteger(definition.version) || definition.version < 1) {
11732
+ throw new Error("Meshy controller-pack version must be a positive integer");
11733
+ }
11734
+ const actionIds = [...definition.actionIds];
11735
+ if (actionIds.some((actionId) => !Number.isInteger(actionId))) {
11736
+ throw new Error("Meshy controller-pack action IDs must be integers");
11737
+ }
11738
+ if (new Set(actionIds).size !== actionIds.length) {
11739
+ throw new Error("Meshy controller-pack action IDs must be unique");
11740
+ }
11741
+ const allowed = new Set(actionIds);
11742
+ const bindings = {};
11743
+ for (const [slot, binding] of Object.entries(definition.bindings).sort(([left], [right]) => left.localeCompare(right))) {
11744
+ if (!slot.trim()) throw new Error("Meshy controller-pack slots cannot be empty");
11745
+ if (!allowed.has(binding.actionId)) {
11746
+ throw new Error(`Meshy controller-pack slot ${slot} refers to action ${binding.actionId} outside its action set`);
11747
+ }
11748
+ if (binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
11749
+ throw new Error(`Meshy controller-pack slot ${slot} has an invalid playback mode`);
11750
+ }
11751
+ if (binding.phase !== void 0 && (!Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
11752
+ throw new Error(`Meshy controller-pack slot ${slot} has an invalid normalized phase`);
11753
+ }
11754
+ bindings[slot] = {
11755
+ actionId: binding.actionId,
11756
+ mode: binding.mode,
11757
+ ...binding.phase === void 0 ? {} : { phase: binding.phase }
11758
+ };
11759
+ }
11760
+ return { key, version: definition.version, actionIds, bindings };
11761
+ }
11762
+ function packFingerprint(definition) {
11763
+ return createHash("sha256").update(JSON.stringify(definition)).digest("hex");
11764
+ }
11765
+ function createMeshyControllerPackSnapshot(definition) {
11766
+ const normalized = normalizedPackDefinition(definition);
11767
+ const actionIds = Object.freeze([...normalized.actionIds]);
11768
+ const bindings = Object.freeze(Object.fromEntries(
11769
+ Object.entries(normalized.bindings).map(([slot, binding]) => [slot, Object.freeze({ ...binding })])
11770
+ ));
11771
+ return Object.freeze({
11772
+ ...normalized,
11773
+ actionIds,
11774
+ bindings,
11775
+ fingerprint: packFingerprint(normalized)
11776
+ });
11777
+ }
11778
+ var NEUTRAL_CONTROLLER_BINDINGS = {
11779
+ "idle.default": { actionId: 243, mode: "loop" },
11780
+ "walk.forward": { actionId: 613, mode: "loop" },
11781
+ "run.forward": { actionId: 657, mode: "loop" },
11782
+ "crouch.forward": { actionId: 616, mode: "loop" },
11783
+ // The reviewed opening frame is a stable lowered stance; the loader samples
11784
+ // it into a separate static clip so crouch movement can keep the full loop.
11785
+ "crouch.idle": { actionId: 616, mode: "pose", phase: 0 },
11786
+ "jump.full": { actionId: 466, mode: "one-shot" }
11787
+ };
11788
+ var MESHY_CONTROLLER_PACK = createMeshyControllerPackSnapshot({
11789
+ key: "neutral-v3",
11790
+ version: 3,
11791
+ actionIds: [243, 613, 657, 616, 466],
11792
+ bindings: NEUTRAL_CONTROLLER_BINDINGS
11793
+ });
11794
+ var MESHY_CONTROLLER_PACK_KEY = MESHY_CONTROLLER_PACK.key;
11795
+ var MESHY_CONTROLLER_PACK_VERSION = MESHY_CONTROLLER_PACK.version;
11796
+ var MESHY_CONTROLLER_PACK_FINGERPRINT = MESHY_CONTROLLER_PACK.fingerprint;
11797
+ var MESHY_CONTROLLER_CORE_ACTION_IDS = MESHY_CONTROLLER_PACK.actionIds;
11798
+ var MESHY_CONTROLLER_BINDINGS = MESHY_CONTROLLER_PACK.bindings;
11799
+ var SYNONYMS = {
11800
+ attack: ["fight", "punch", "kick", "weapon", "combat"],
11801
+ combat: ["fight", "attack", "punch", "weapon"],
11802
+ crouch: ["sneak", "stealth"],
11803
+ die: ["death", "dying", "fall"],
11804
+ emote: ["gesture", "acting", "dance"],
11805
+ gun: ["rifle", "pistol", "shoot", "firearm"],
11806
+ idle: ["stand", "breathing"],
11807
+ jump: ["leap", "vault"],
11808
+ run: ["running", "jog", "sprint", "charge"],
11809
+ sit: ["sitting", "chair", "seat"],
11810
+ skate: ["skateboard", "skating", "board"],
11811
+ sword: ["blade", "weapon", "slash"],
11812
+ walk: ["walking", "stride", "stroll"],
11813
+ wave: ["hello", "greeting", "gesture"]
11814
+ };
11815
+ var STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "and", "for", "of", "the", "to", "with"]);
11816
+ function tokens(value) {
11817
+ return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((token) => token && !STOP_WORDS.has(token));
11818
+ }
11819
+ function tokenGroup(token) {
11820
+ const group = /* @__PURE__ */ new Set([token, ...SYNONYMS[token] ?? []]);
11821
+ for (const [canonical, synonyms] of Object.entries(SYNONYMS)) {
11822
+ if (synonyms.includes(token)) {
11823
+ group.add(canonical);
11824
+ for (const synonym of synonyms) group.add(synonym);
11825
+ }
11826
+ }
11827
+ return group;
11828
+ }
11829
+ var MESHY_ANIMATIONS = MESHY_ANIMATION_CATALOG.map(curateMeshyAnimation);
11830
+ var MESHY_ANIMATIONS_BY_ID = new Map(MESHY_ANIMATIONS.map((entry) => [entry.actionId, entry]));
11831
+ var MESHY_ANIMATIONS_BY_KEY = new Map(MESHY_ANIMATIONS.map((entry) => [entry.key.toLowerCase(), entry]));
11832
+ function animationById(actionId) {
11833
+ return MESHY_ANIMATIONS_BY_ID.get(actionId);
11834
+ }
11835
+ function searchMeshyAnimations(query, options = {}) {
11836
+ const rawQuery = query.trim().toLowerCase();
11837
+ const queryTokens = tokens(query);
11838
+ const groups = queryTokens.map(tokenGroup);
11839
+ const expanded = new Set(groups.flatMap((group) => [...group]));
11840
+ const results = [];
11841
+ for (const entry of MESHY_ANIMATIONS) {
11842
+ if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
11843
+ if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
11844
+ const matched = [];
11845
+ let score = 0;
11846
+ if (String(entry.actionId) === rawQuery) {
11847
+ score += 1e5;
11848
+ matched.push("action id");
11849
+ }
11850
+ if (entry.key.toLowerCase() === rawQuery) {
11851
+ score += 8e4;
11852
+ matched.push("stable key");
11853
+ }
11854
+ if (entry.name.toLowerCase() === rawQuery) {
11855
+ score += 6e4;
11856
+ matched.push("exact name");
11857
+ }
11858
+ const haystack = new Set(tokens([
11859
+ entry.key,
11860
+ entry.name,
11861
+ entry.category,
11862
+ entry.subCategory,
11863
+ ...entry.aliases,
11864
+ ...entry.gameplayTags
11865
+ ].join(" ")));
11866
+ let coveredGroups = 0;
11867
+ for (let index = 0; index < groups.length; index++) {
11868
+ const hits = [...groups[index]].filter((token) => haystack.has(token));
11869
+ if (hits.length === 0) continue;
11870
+ coveredGroups++;
11871
+ const original = queryTokens[index];
11872
+ const best = hits.includes(original) ? original : hits[0];
11873
+ score += best === original ? 400 : 140;
11874
+ matched.push(best);
11875
+ }
11876
+ if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score += 35;
11877
+ const exact = score >= 6e4;
11878
+ if (score > 0 && (exact || coveredGroups === groups.length)) {
11879
+ results.push({ entry, score, matched: [...new Set(matched)] });
11880
+ }
11881
+ }
11882
+ return results.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name) || a.entry.actionId - b.entry.actionId).slice(0, options.limit ?? 20);
11883
+ }
11884
+
11885
+ // src/lib/anims.ts
11886
+ import fs11 from "fs/promises";
11887
+ import path12 from "path";
11888
+ var ANIMS_DEST = path12.join("public", "assets", "anims");
11889
+ var HIDDEN_TAG = "reference";
11890
+ async function runAnims(opts) {
11891
+ const log = createLogger({ quiet: opts.quiet });
11892
+ const root = opts.cwd ?? process.cwd();
11893
+ const selectors = opts.selectors ?? [];
11894
+ log.plain(c.bold("genex controller anims"));
11895
+ log.plain("");
11896
+ const { manifest, source } = await loadManifest(opts.animsBase);
11897
+ if (source === "snapshot") {
11898
+ log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
11899
+ }
11900
+ if (opts.list) {
11901
+ printCatalog(log, manifest, selectors);
11902
+ return;
11903
+ }
11904
+ const controllerMarker = path12.join(root, "src", "controllers", "character");
11905
+ if (!await exists2(controllerMarker)) {
11906
+ log.error(
11907
+ `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
11908
+ );
11909
+ log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
11910
+ process.exitCode = 1;
11911
+ return;
11912
+ }
11913
+ const destDir = path12.join(root, ANIMS_DEST);
11914
+ const gameManifestPath = path12.join(destDir, "manifest.json");
11915
+ if (opts.reset) {
11916
+ await fs11.rm(destDir, { recursive: true, force: true });
11917
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
11918
+ }
11919
+ if (selectors.length === 0) {
11920
+ const installed = await readGameManifest(gameManifestPath);
11921
+ if (installed === null || installed.clips.length === 0) {
11922
+ log.plain(" No animation packs installed yet.");
11923
+ } else {
11924
+ log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
11925
+ }
11926
+ log.plain("");
11927
+ log.plain(
11928
+ ` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
11929
+ "genex controller anims --list"
11930
+ )}.`
11931
+ );
11932
+ return;
11933
+ }
11934
+ let resolved;
11935
+ try {
11936
+ resolved = resolveSelectors(manifest, selectors);
11937
+ } catch (err) {
11938
+ log.error(err instanceof Error ? err.message : String(err));
11939
+ process.exitCode = 1;
11940
+ return;
11941
+ }
11942
+ const coreNames = new Set(manifest.core);
11943
+ const byName = /* @__PURE__ */ new Map();
11944
+ let bundledSkips = 0;
11945
+ for (const entries of resolved.values()) {
11946
+ for (const entry of entries) {
11947
+ if (coreNames.has(entry.name)) {
11948
+ bundledSkips++;
11949
+ continue;
11950
+ }
11951
+ byName.set(entry.name, entry);
11952
+ }
11953
+ }
11954
+ const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
11955
+ const cacheDir = path12.join(
11956
+ opts.cacheDir ?? getAnimsCacheDir(),
11957
+ `${manifest.library}-v${manifest.version}`
11958
+ );
11959
+ await fs11.mkdir(cacheDir, { recursive: true });
11960
+ await fs11.mkdir(destDir, { recursive: true });
11961
+ const base = getAnimsBase(opts.animsBase);
11962
+ let installedCount = 0;
11963
+ let presentCount = 0;
11964
+ let addedBytes = 0;
11965
+ const failures = [];
11966
+ for (const entry of wanted) {
11967
+ const dest = path12.join(destDir, entry.file);
11968
+ if (await hasSize(dest, entry.bytes)) {
11969
+ presentCount++;
11970
+ continue;
11971
+ }
11972
+ try {
11973
+ const cached = path12.join(cacheDir, entry.file);
11974
+ if (!await hasSize(cached, entry.bytes)) {
11975
+ const res = await fetch(base + entry.file);
11976
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
11977
+ const buf = Buffer.from(await res.arrayBuffer());
11978
+ await fs11.writeFile(cached, buf);
11979
+ }
11980
+ await fs11.copyFile(cached, dest);
11981
+ installedCount++;
11982
+ addedBytes += entry.bytes;
11983
+ log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
11984
+ } catch (err) {
11985
+ failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
11986
+ }
11987
+ }
11988
+ const previous = await readGameManifest(gameManifestPath);
11989
+ const union = new Set(previous?.clips ?? []);
11990
+ for (const entry of wanted) {
11991
+ if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
11992
+ }
11993
+ const gameManifest = {
11994
+ schema: 1,
11995
+ library: manifest.library,
11996
+ version: manifest.version,
11997
+ clips: [...union].sort((a, b) => a.localeCompare(b))
11998
+ };
11999
+ await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
12000
+ log.plain("");
12001
+ const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
12002
+ if (presentCount > 0) parts.push(`${presentCount} already present`);
12003
+ if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
12004
+ log.success(
12005
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
12006
+ );
12007
+ for (const [selector, entries] of resolved) {
12008
+ const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
12009
+ if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
12010
+ }
12011
+ log.info(
12012
+ `Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
12013
+ );
12014
+ if (failures.length > 0) {
12015
+ log.plain("");
12016
+ log.error(
12017
+ `${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
12018
+ );
12019
+ log.plain(
12020
+ " Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
12021
+ );
12022
+ process.exitCode = 1;
12023
+ }
12024
+ }
12025
+ async function loadManifest(baseOverride) {
12026
+ const base = getAnimsBase(baseOverride);
12027
+ try {
12028
+ const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
12029
+ if (res.ok) {
12030
+ const manifest2 = await res.json();
12031
+ if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
12032
+ return { manifest: manifest2, source: "cdn" };
12033
+ }
12034
+ }
12035
+ } catch {
12036
+ }
12037
+ const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
12038
+ const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
12039
+ return { manifest, source: "snapshot" };
12040
+ }
12041
+ function resolveSelectors(manifest, selectors) {
12042
+ const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
12043
+ const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
12044
+ const tags = /* @__PURE__ */ new Map();
12045
+ for (const entry of manifest.clips) {
12046
+ for (const tag of entry.tags) {
12047
+ const list = tags.get(tag) ?? [];
12048
+ list.push(entry);
12049
+ tags.set(tag, list);
12050
+ }
12051
+ }
12052
+ const out = /* @__PURE__ */ new Map();
12053
+ for (const selector of selectors) {
12054
+ const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
12055
+ if (exact) {
12056
+ out.set(selector, [exact]);
12057
+ continue;
12058
+ }
12059
+ const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
12060
+ if (tagHit) {
12061
+ out.set(selector, tagHit);
12062
+ continue;
12063
+ }
12064
+ const candidates = [...tags.keys(), ...byName.keys()];
12065
+ const close = suggest(selector, candidates);
12066
+ throw new Error(
12067
+ `unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
12068
+ "genex controller anims --list"
12069
+ )} for the catalog.`
12070
+ );
12071
+ }
12072
+ return out;
12073
+ }
12074
+ function suggest(input, candidates) {
12075
+ const lower = input.toLowerCase();
12076
+ const scored = [];
12077
+ for (const candidate of candidates) {
12078
+ const candidateLower = candidate.toLowerCase();
12079
+ if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
12080
+ scored.push({ name: candidate, score: 0 });
12081
+ continue;
12082
+ }
12083
+ const distance = levenshtein(lower, candidateLower, 2);
12084
+ if (distance <= 2) scored.push({ name: candidate, score: distance });
12085
+ }
12086
+ scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
12087
+ return scored.slice(0, 4).map((s) => s.name);
12088
+ }
12089
+ function levenshtein(a, b, max) {
12090
+ if (Math.abs(a.length - b.length) > max) return max + 1;
12091
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
12092
+ for (let i = 1; i <= a.length; i++) {
12093
+ const curr = [i];
12094
+ let rowMin = i;
12095
+ for (let j = 1; j <= b.length; j++) {
12096
+ curr[j] = Math.min(
12097
+ prev[j] + 1,
12098
+ curr[j - 1] + 1,
12099
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
12100
+ );
12101
+ if (curr[j] < rowMin) rowMin = curr[j];
12102
+ }
12103
+ if (rowMin > max) return max + 1;
12104
+ prev = curr;
12105
+ }
12106
+ return prev[b.length];
12107
+ }
12108
+ function printCatalog(log, manifest, selectors) {
12109
+ const coreNames = new Set(manifest.core);
12110
+ if (selectors.length > 0) {
12111
+ let resolved;
12112
+ try {
12113
+ resolved = resolveSelectors(manifest, selectors);
12114
+ } catch (err) {
12115
+ log.error(err instanceof Error ? err.message : String(err));
12116
+ process.exitCode = 1;
12117
+ return;
12118
+ }
12119
+ for (const [selector, entries] of resolved) {
12120
+ log.plain(c.bold(selector));
12121
+ for (const entry of entries) {
12122
+ const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
12123
+ log.plain(
12124
+ ` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
12125
+ );
12126
+ }
12127
+ }
12128
+ return;
12129
+ }
12130
+ const tags = /* @__PURE__ */ new Map();
12131
+ for (const entry of manifest.clips) {
12132
+ for (const tag of entry.tags) {
12133
+ if (tag === HIDDEN_TAG) continue;
12134
+ const list = tags.get(tag) ?? [];
12135
+ list.push(entry);
12136
+ tags.set(tag, list);
12137
+ }
12138
+ }
12139
+ log.plain(
12140
+ `${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
12141
+ );
12142
+ log.plain(
12143
+ ` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
12144
+ "genex controller anims --list <tag>"
12145
+ )}`
12146
+ );
12147
+ log.plain("");
12148
+ for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
12149
+ const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
12150
+ const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
12151
+ log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
12245
12152
  }
12246
- };
12247
- var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
12248
- var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
12249
- "Climbing",
12250
- "HangingfromLedge",
12251
- "VaultingOverObstacle",
12252
- "Interacting",
12253
- "PickingUpItem",
12254
- "Pushing",
12255
- "Sleeping"
12256
- ]);
12257
- var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
12258
- var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
12259
- function words(value) {
12260
- return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
12261
12153
  }
12262
- function inferRequirements(key, subCategory) {
12263
- const lower = `${key} ${subCategory}`.toLowerCase().replace(/[^a-z0-9]+/g, " ");
12264
- const props = [
12265
- ["gun", /\b(?:gun|rifle|pistol)\b/],
12266
- ["sword", /\b(?:sword|blade)\b/],
12267
- ["bow", /\b(?:bow|arrow)\b/],
12268
- ["shield", /\bshield\b/],
12269
- ["chair", /\b(?:chair|sit|sitting)\b/],
12270
- ["ladder", /\bladder\b/],
12271
- ["rope", /\brope\b/]
12272
- ].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
12273
- const environment = [
12274
- ["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
12275
- ["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
12276
- ["water", /\b(?:swim|swimming)\b/],
12277
- ["seat", /\b(?:sit|sitting|chair)\b/]
12278
- ].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
12279
- return { props, environment, partner: /\b(?:partner|carry person|hug|handshake)\b/.test(lower) };
12154
+ async function readGameManifest(file) {
12155
+ try {
12156
+ return JSON.parse(await fs11.readFile(file, "utf8"));
12157
+ } catch {
12158
+ return null;
12159
+ }
12280
12160
  }
12281
- function inferSlots(key) {
12282
- const lower = key.toLowerCase();
12283
- const band = lower.includes("crouch") ? "crouch" : lower.includes("run") || lower.includes("sprint") ? "run" : lower.includes("walk") ? "walk" : null;
12284
- if (!band) return [];
12285
- const direction = lower.includes("backleft") ? "back-left" : lower.includes("backright") ? "back-right" : lower.includes("backward") || lower.includes("_back") ? "backward" : lower.includes("left") ? "left" : lower.includes("right") ? "right" : "forward";
12286
- return [`${band}.${direction}`];
12161
+ async function hasSize(file, bytes) {
12162
+ try {
12163
+ return (await fs11.stat(file)).size === bytes;
12164
+ } catch {
12165
+ return false;
12166
+ }
12287
12167
  }
12288
- function curateMeshyAnimation(raw) {
12289
- const inPlace = raw.tag === "InPlace";
12290
- const requirements = inferRequirements(raw.key, raw.subCategory);
12291
- const choreography = CHOREOGRAPHY_SUBCATEGORIES.has(raw.subCategory) || requirements.partner;
12292
- const loop = inPlace && LOOP_SUBCATEGORIES.has(raw.subCategory) && !NON_LOOP_ACTION.test(`${raw.key} ${raw.name}`);
12293
- const motionPolicy = loop ? "controller-loop" : choreography ? "choreography" : PLANAR_ACTION.test(`${raw.key} ${raw.name}`) ? "planar-root-action" : "anchored-action";
12294
- const base = {
12295
- actionId: raw.actionId,
12296
- key: raw.key,
12297
- name: raw.name,
12298
- category: raw.category,
12299
- subCategory: raw.subCategory,
12300
- previewUrl: raw.previewUrl,
12301
- rigType: raw.rigType,
12302
- inPlace,
12303
- isDefault: raw.isDefault,
12304
- isFree: raw.isFree,
12305
- createdAt: raw.createdAt,
12306
- aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
12307
- gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
12308
- loop,
12309
- motionPolicy,
12310
- rootMotionValidated: false,
12311
- // A name that contains "walk" or "run" is not enough to make a safe
12312
- // controller loop. Only provider-declared InPlace loops (plus explicit
12313
- // measured overrides above) may populate automatic locomotion slots.
12314
- controllerSlots: loop ? inferSlots(raw.key) : [],
12315
- requirements,
12316
- reviewStatus: "metadata-reviewed"
12317
- };
12318
- return { ...base, ...CURATED[raw.actionId], requirements };
12168
+ async function exists2(p) {
12169
+ try {
12170
+ await fs11.access(p);
12171
+ return true;
12172
+ } catch {
12173
+ return false;
12174
+ }
12175
+ }
12176
+ function formatMb(bytes) {
12177
+ return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
12319
12178
  }
12320
12179
 
12321
- // ../../packages/meshy-animation-catalog/src/index.ts
12322
- var SYNONYMS = {
12323
- attack: ["fight", "punch", "kick", "weapon", "combat"],
12324
- combat: ["fight", "attack", "punch", "weapon"],
12325
- crouch: ["sneak", "stealth"],
12326
- die: ["death", "dying", "fall"],
12327
- emote: ["gesture", "acting", "dance"],
12328
- gun: ["rifle", "pistol", "shoot", "firearm"],
12329
- idle: ["stand", "breathing"],
12330
- jump: ["leap", "vault"],
12331
- run: ["running", "jog", "sprint", "charge"],
12332
- sit: ["sitting", "chair", "seat"],
12333
- skate: ["skateboard", "skating", "board"],
12334
- sword: ["blade", "weapon", "slash"],
12335
- walk: ["walking", "stride", "stroll"],
12336
- wave: ["hello", "greeting", "gesture"]
12180
+ // src/commands/controller.ts
12181
+ var CONTROLLER_KINDS = [
12182
+ "character",
12183
+ "car",
12184
+ "drone",
12185
+ "touch",
12186
+ "networked-physics"
12187
+ ];
12188
+ var SHARED = [
12189
+ "shared/math.ts",
12190
+ "shared/physics-world.ts",
12191
+ "shared/colliders.ts"
12192
+ ];
12193
+ var TOUCH_KIT = [
12194
+ "touch/touch-joystick.ts",
12195
+ "touch/drag-zone.ts",
12196
+ "touch/rotate-overlay.ts"
12197
+ ];
12198
+ var INPUT_AND_CAMERA = [
12199
+ "character/follow-camera.ts",
12200
+ "character/keyboard-input.ts",
12201
+ "character/touch-joystick.ts",
12202
+ ...TOUCH_KIT
12203
+ ];
12204
+ var NOTICE = "NOTICE.md";
12205
+ var CONTROLLER_FILE_SETS = {
12206
+ character: {
12207
+ code: [
12208
+ ...SHARED,
12209
+ "character/character-controller.ts",
12210
+ "character/character-animations.ts",
12211
+ "character/animation-packs.ts",
12212
+ "character/motion-actions.ts",
12213
+ "character/meshy/meshy-loader.ts",
12214
+ "character/presets.ts",
12215
+ // VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
12216
+ // the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
12217
+ "character/vrm/vrm-loader.ts",
12218
+ "character/vrm/vrm-retarget.ts",
12219
+ "character/vrm/capsule-fit.ts",
12220
+ "character/vrm/foot-ik.ts",
12221
+ ...INPUT_AND_CAMERA,
12222
+ NOTICE
12223
+ ],
12224
+ // The player's VRM is written to public/assets/avatar.vrm at install time by
12225
+ // installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
12226
+ // a static manifest asset. animation-library.glb (the 12-clip core) still is;
12227
+ // extra clips arrive via `genex controller anims` into public/assets/anims/.
12228
+ assets: ["assets/animation-library.glb"],
12229
+ skill: "genex-threejs-character-controller",
12230
+ sketch: [
12231
+ `const physics = await PhysicsWorld.create();`,
12232
+ `const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
12233
+ `const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
12234
+ `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
12235
+ `character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
12236
+ `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
12237
+ ]
12238
+ },
12239
+ car: {
12240
+ code: [
12241
+ ...SHARED,
12242
+ "vehicle/vehicle-controller.ts",
12243
+ "vehicle/wheel.ts",
12244
+ "vehicle/presets.ts",
12245
+ "interact/enter-exit.ts",
12246
+ ...INPUT_AND_CAMERA,
12247
+ NOTICE
12248
+ ],
12249
+ assets: [],
12250
+ skill: "genex-threejs-vehicle-controllers",
12251
+ sketch: [
12252
+ `const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
12253
+ `const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
12254
+ `scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
12255
+ ]
12256
+ },
12257
+ drone: {
12258
+ code: [
12259
+ ...SHARED,
12260
+ "drone/drone-controller.ts",
12261
+ "drone/presets.ts",
12262
+ "interact/enter-exit.ts",
12263
+ ...INPUT_AND_CAMERA,
12264
+ NOTICE
12265
+ ],
12266
+ assets: [],
12267
+ skill: "genex-threejs-vehicle-controllers",
12268
+ sketch: [
12269
+ `const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
12270
+ `const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
12271
+ `physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
12272
+ ]
12273
+ },
12274
+ touch: {
12275
+ code: [...TOUCH_KIT, NOTICE],
12276
+ assets: [],
12277
+ skill: "genex-threejs-touch-controls",
12278
+ sketch: [
12279
+ `const joy = new TouchJoystick({ floating: true }); // safe-area-aware defaults; static circle without the flag`,
12280
+ `const jump = new VirtualButton({ label: "Jump", onPress: () => player.jump() });`,
12281
+ `const look = new DragZone(); // right half; per frame: const { dx, dy } = look.consumeDelta()`,
12282
+ `[joy, jump, look].forEach((w) => w.setVisible(navigator.maxTouchPoints > 0));`
12283
+ ]
12284
+ },
12285
+ "networked-physics": {
12286
+ code: [
12287
+ ...SHARED,
12288
+ "network/pose.ts",
12289
+ "network/networked-pushable.ts",
12290
+ "network/networked-vehicle.ts",
12291
+ "NETWORKING.md",
12292
+ NOTICE
12293
+ ],
12294
+ assets: [],
12295
+ skill: "genex-threejs-multiplayer",
12296
+ sketch: [
12297
+ `const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
12298
+ `physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
12299
+ `contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
12300
+ ]
12301
+ }
12337
12302
  };
12338
- var STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "and", "for", "of", "the", "to", "with"]);
12339
- function tokens(value) {
12340
- return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((token) => token && !STOP_WORDS.has(token));
12341
- }
12342
- function tokenGroup(token) {
12343
- const group = /* @__PURE__ */ new Set([token, ...SYNONYMS[token] ?? []]);
12344
- for (const [canonical, synonyms] of Object.entries(SYNONYMS)) {
12345
- if (synonyms.includes(token)) {
12346
- group.add(canonical);
12347
- for (const synonym of synonyms) group.add(synonym);
12303
+ var CODE_DEST = path13.join("src", "controllers");
12304
+ var ASSETS_DEST = path13.join("public", "assets");
12305
+ async function runController(opts) {
12306
+ const log = createLogger({ quiet: opts.quiet });
12307
+ if (opts.kind?.trim() === "anims") {
12308
+ await runAnims(opts);
12309
+ return;
12310
+ }
12311
+ const kind = opts.kind?.trim();
12312
+ if (!kind || !CONTROLLER_KINDS.includes(kind)) {
12313
+ log.error(
12314
+ `Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
12315
+ "genex controller <character|car|drone|touch|networked-physics> [--force]"
12316
+ )} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
12317
+ );
12318
+ process.exitCode = 1;
12319
+ return;
12320
+ }
12321
+ const srcDir = path13.join(getTemplatesDir(), "controllers");
12322
+ const root = opts.cwd ?? process.cwd();
12323
+ const set = CONTROLLER_FILE_SETS[kind];
12324
+ log.plain(c.bold(`genex controller ${kind}`));
12325
+ log.plain("");
12326
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
12327
+ const plan = [
12328
+ ...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
12329
+ ...set.assets.map((rel) => ({
12330
+ from: rel,
12331
+ rel: path13.join(ASSETS_DEST, path13.basename(rel))
12332
+ }))
12333
+ ];
12334
+ let copied = 0;
12335
+ let skipped = 0;
12336
+ try {
12337
+ for (const file of plan) {
12338
+ const dest = path13.join(root, file.rel);
12339
+ if (!opts.force && await exists3(dest)) {
12340
+ skipped++;
12341
+ log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
12342
+ continue;
12343
+ }
12344
+ await fs12.mkdir(path13.dirname(dest), { recursive: true });
12345
+ await fs12.copyFile(path13.join(srcDir, file.from), dest);
12346
+ copied++;
12347
+ log.dim(` ${file.rel}`);
12348
12348
  }
12349
+ } catch (err) {
12350
+ log.error(`Copy failed: ${String(err)}`);
12351
+ process.exitCode = 1;
12352
+ return;
12349
12353
  }
12350
- return group;
12351
- }
12352
- var MESHY_ANIMATIONS = MESHY_ANIMATION_CATALOG.map(curateMeshyAnimation);
12353
- var MESHY_ANIMATIONS_BY_ID = new Map(MESHY_ANIMATIONS.map((entry) => [entry.actionId, entry]));
12354
- var MESHY_ANIMATIONS_BY_KEY = new Map(MESHY_ANIMATIONS.map((entry) => [entry.key.toLowerCase(), entry]));
12355
- function animationById(actionId) {
12356
- return MESHY_ANIMATIONS_BY_ID.get(actionId);
12357
- }
12358
- function searchMeshyAnimations(query, options = {}) {
12359
- const rawQuery = query.trim().toLowerCase();
12360
- const queryTokens = tokens(query);
12361
- const groups = queryTokens.map(tokenGroup);
12362
- const expanded = new Set(groups.flatMap((group) => [...group]));
12363
- const results = [];
12364
- for (const entry of MESHY_ANIMATIONS) {
12365
- if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
12366
- if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
12367
- const matched = [];
12368
- let score = 0;
12369
- if (String(entry.actionId) === rawQuery) {
12370
- score += 1e5;
12371
- matched.push("action id");
12354
+ log.success(
12355
+ `Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
12356
+ );
12357
+ log.plain("");
12358
+ if (kind === "character") {
12359
+ if (opts.character) {
12360
+ try {
12361
+ const token = opts.token !== void 0 ? opts.token : await readUserToken();
12362
+ if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
12363
+ await installMeshyCharacterManifest({
12364
+ root,
12365
+ characterId: opts.character,
12366
+ apiUrl: getApiUrl(opts.apiUrl),
12367
+ token,
12368
+ log
12369
+ });
12370
+ } catch (error) {
12371
+ log.error(error instanceof Error ? error.message : String(error));
12372
+ process.exitCode = 1;
12373
+ return;
12374
+ }
12375
+ } else {
12376
+ const token = opts.token !== void 0 ? opts.token : await readUserToken();
12377
+ await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
12372
12378
  }
12373
- if (entry.key.toLowerCase() === rawQuery) {
12374
- score += 8e4;
12375
- matched.push("stable key");
12379
+ log.plain("");
12380
+ }
12381
+ log.plain(c.bold("Next steps"));
12382
+ if (kind !== "touch") {
12383
+ log.plain(
12384
+ ` 1. ${c.cyan(
12385
+ kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
12386
+ )} (three is already in the scaffold).`
12387
+ );
12388
+ }
12389
+ const stepOffset = kind === "touch" ? 0 : 1;
12390
+ log.plain(
12391
+ ` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
12392
+ );
12393
+ log.plain(
12394
+ kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
12395
+ );
12396
+ const sketch = kind === "character" && opts.character ? [
12397
+ `const physics = await PhysicsWorld.create();`,
12398
+ `const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
12399
+ `const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
12400
+ `character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
12401
+ `physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
12402
+ ] : set.sketch;
12403
+ for (const line of sketch) {
12404
+ log.dim(` ${line}`);
12405
+ }
12406
+ await firstPreviewNudge(log);
12407
+ }
12408
+ async function installMeshyCharacterManifest(args) {
12409
+ const response = await apiFetch(
12410
+ `${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
12411
+ { headers: { Authorization: `Bearer ${args.token}` } }
12412
+ );
12413
+ if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
12414
+ if (!response.ok) {
12415
+ let detail;
12416
+ try {
12417
+ const errorBody = await response.json();
12418
+ if (typeof errorBody.message === "string" && errorBody.message.length > 0) detail = errorBody.message;
12419
+ else if (typeof errorBody.error === "string" && errorBody.error.length > 0) detail = errorBody.error;
12420
+ } catch {
12376
12421
  }
12377
- if (entry.name.toLowerCase() === rawQuery) {
12378
- score += 6e4;
12379
- matched.push("exact name");
12422
+ throw new Error(detail ?? `Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
12423
+ }
12424
+ const body = await response.json();
12425
+ const manifest = body.manifest;
12426
+ if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
12427
+ throw new Error("The API returned an invalid Meshy character manifest.");
12428
+ }
12429
+ assertCompleteMeshyControllerPack(manifest);
12430
+ const destination = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
12431
+ await fs12.mkdir(path13.dirname(destination), { recursive: true });
12432
+ await fs12.writeFile(
12433
+ destination,
12434
+ `${JSON.stringify(manifest, null, 2)}
12435
+ `
12436
+ );
12437
+ args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
12438
+ const pack = manifest.controllerPack;
12439
+ if (typeof pack?.key === "string" && typeof pack.version === "number") {
12440
+ args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
12441
+ } else {
12442
+ args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
12443
+ }
12444
+ const provenance = manifest.provenance;
12445
+ if (provenance?.provider === "meshy" && typeof provenance.aiModel === "string") {
12446
+ const apiVersion = typeof provenance.apiVersion === "string" && provenance.apiVersion.length > 0 ? `; API ${provenance.apiVersion}` : "";
12447
+ args.log.success(`Meshy model: ${provenance.aiModel}${apiVersion}`);
12448
+ if ((provenance.poseMode === "a-pose" || provenance.poseMode === "t-pose") && typeof provenance.shouldRemesh === "boolean" && typeof provenance.targetPolycount === "number") {
12449
+ args.log.dim(
12450
+ ` generation: ${provenance.poseMode}, remesh ${provenance.shouldRemesh ? "on" : "off"}, target ${provenance.targetPolycount} polygons`
12451
+ );
12380
12452
  }
12381
- const haystack = new Set(tokens([
12382
- entry.key,
12383
- entry.name,
12384
- entry.category,
12385
- entry.subCategory,
12386
- ...entry.aliases,
12387
- ...entry.gameplayTags
12388
- ].join(" ")));
12389
- let coveredGroups = 0;
12390
- for (let index = 0; index < groups.length; index++) {
12391
- const hits = [...groups[index]].filter((token) => haystack.has(token));
12392
- if (hits.length === 0) continue;
12393
- coveredGroups++;
12394
- const original = queryTokens[index];
12395
- const best = hits.includes(original) ? original : hits[0];
12396
- score += best === original ? 400 : 140;
12397
- matched.push(best);
12453
+ } else {
12454
+ args.log.warn("Meshy model/version metadata is unavailable in this legacy manifest.");
12455
+ }
12456
+ const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
12457
+ args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
12458
+ const bindings = manifest.locomotion?.bindings;
12459
+ if (bindings) {
12460
+ for (const [slot, binding] of Object.entries(bindings).sort(([a], [b]) => a.localeCompare(b))) {
12461
+ if (typeof binding?.actionId === "number" && typeof binding.clip === "string" && typeof binding.mode === "string") {
12462
+ const phase = typeof binding.phase === "number" ? ` @ phase ${binding.phase}` : "";
12463
+ args.log.dim(` ${slot} -> action ${binding.actionId} (${binding.clip}, ${binding.mode}${phase})`);
12464
+ }
12398
12465
  }
12399
- if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score += 35;
12400
- const exact = score >= 6e4;
12401
- if (score > 0 && (exact || coveredGroups === groups.length)) {
12402
- results.push({ entry, score, matched: [...new Set(matched)] });
12466
+ }
12467
+ const slots = Object.keys(bindings ?? manifest.locomotion?.slots ?? {}).sort();
12468
+ args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
12469
+ const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
12470
+ if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
12471
+ else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
12472
+ }
12473
+ var REQUIRED_MESHY_CONTROLLER_SLOTS = [
12474
+ "idle.default",
12475
+ "walk.forward",
12476
+ "run.forward",
12477
+ "crouch.forward",
12478
+ "crouch.idle",
12479
+ "jump.full"
12480
+ ];
12481
+ function incompletePack(message) {
12482
+ throw new Error(`Incomplete Meshy controller pack: ${message}`);
12483
+ }
12484
+ function assertCompleteMeshyControllerPack(manifest) {
12485
+ const pack = manifest.controllerPack;
12486
+ if (pack === void 0) return;
12487
+ if (typeof pack.key !== "string" || pack.key.length === 0 || typeof pack.version !== "number" || !Number.isInteger(pack.version)) {
12488
+ incompletePack("invalid key or version.");
12489
+ }
12490
+ if (typeof pack.fingerprint !== "string" || pack.fingerprint.length === 0) {
12491
+ incompletePack(`${pack.key} v${pack.version} has no immutable fingerprint.`);
12492
+ }
12493
+ if (!Array.isArray(pack.actionIds) || pack.actionIds.length === 0 || pack.actionIds.some((actionId) => typeof actionId !== "number" || !Number.isInteger(actionId))) {
12494
+ incompletePack(`${pack.key} v${pack.version} has no valid action snapshot.`);
12495
+ }
12496
+ const actionIds = pack.actionIds;
12497
+ const packActionIds = new Set(actionIds);
12498
+ if (packActionIds.size !== actionIds.length) {
12499
+ incompletePack(`${pack.key} v${pack.version} repeats an action ID.`);
12500
+ }
12501
+ const bindings = manifest.locomotion?.bindings;
12502
+ if (!bindings) incompletePack(`${pack.key} v${pack.version} has no authoritative locomotion bindings.`);
12503
+ const missingSlots = REQUIRED_MESHY_CONTROLLER_SLOTS.filter((slot) => bindings[slot] === void 0);
12504
+ if (missingSlots.length > 0) {
12505
+ incompletePack(`${pack.key} v${pack.version} is missing ${missingSlots.join(", ")}.`);
12506
+ }
12507
+ const clips = manifest.clips ?? [];
12508
+ const boundActionIds = /* @__PURE__ */ new Set();
12509
+ const snapshotBindings = {};
12510
+ for (const [slot, binding] of Object.entries(bindings)) {
12511
+ if (typeof binding.actionId !== "number" || !Number.isInteger(binding.actionId) || typeof binding.clip !== "string" || binding.clip.length === 0 || binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
12512
+ incompletePack(`${pack.key} v${pack.version} has an invalid ${slot} binding.`);
12513
+ }
12514
+ if (binding.phase !== void 0 && (typeof binding.phase !== "number" || !Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
12515
+ incompletePack(`${pack.key} v${pack.version} has an invalid ${slot} phase.`);
12516
+ }
12517
+ const actionId = binding.actionId;
12518
+ boundActionIds.add(actionId);
12519
+ if (!packActionIds.has(actionId)) {
12520
+ incompletePack(`${slot} points at action ${actionId}, which is outside the stored pack snapshot.`);
12521
+ }
12522
+ const clip = clips.find((entry) => entry.actionId === actionId);
12523
+ if (!clip || clip.key !== binding.clip) {
12524
+ incompletePack(`${slot} expects action ${actionId} clip ${binding.clip}, but that exact clip is not installed.`);
12525
+ }
12526
+ snapshotBindings[slot] = {
12527
+ actionId,
12528
+ mode: binding.mode,
12529
+ ...binding.phase === void 0 ? {} : { phase: binding.phase }
12530
+ };
12531
+ }
12532
+ const unboundActionIds = actionIds.filter((actionId) => !boundActionIds.has(actionId));
12533
+ if (unboundActionIds.length > 0) {
12534
+ incompletePack(`${pack.key} v${pack.version} action snapshot contains unbound actions ${unboundActionIds.join(", ")}.`);
12535
+ }
12536
+ let computedFingerprint;
12537
+ try {
12538
+ computedFingerprint = createMeshyControllerPackSnapshot({
12539
+ key: pack.key,
12540
+ version: pack.version,
12541
+ actionIds,
12542
+ bindings: snapshotBindings
12543
+ }).fingerprint;
12544
+ } catch (error) {
12545
+ incompletePack(error instanceof Error ? error.message : "the immutable snapshot is invalid.");
12546
+ }
12547
+ if (computedFingerprint !== pack.fingerprint) {
12548
+ incompletePack(`${pack.key} v${pack.version} fingerprint does not match its action bindings.`);
12549
+ }
12550
+ }
12551
+ async function installOwnerAvatar(args) {
12552
+ const { root, srcDir, apiUrl, token, log } = args;
12553
+ const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
12554
+ await fs12.mkdir(path13.dirname(dest), { recursive: true });
12555
+ if (token) {
12556
+ try {
12557
+ const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
12558
+ headers: { Authorization: `Bearer ${token}` }
12559
+ });
12560
+ if (res.ok) {
12561
+ const me = await res.json();
12562
+ if (me.vrmUrl) {
12563
+ const vrmRes = await fetch(me.vrmUrl);
12564
+ if (vrmRes.ok) {
12565
+ const buf = Buffer.from(await vrmRes.arrayBuffer());
12566
+ await fs12.writeFile(dest, buf);
12567
+ log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
12568
+ return;
12569
+ }
12570
+ }
12571
+ }
12572
+ log.dim(" couldn't fetch your avatar; using the bundled default.");
12573
+ } catch {
12574
+ log.dim(" avatar fetch failed (offline?); using the bundled default.");
12403
12575
  }
12404
12576
  }
12405
- return results.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name) || a.entry.actionId - b.entry.actionId).slice(0, options.limit ?? 20);
12577
+ await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
12578
+ log.dim(
12579
+ token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
12580
+ );
12581
+ }
12582
+ async function exists3(p) {
12583
+ try {
12584
+ await fs12.access(p);
12585
+ return true;
12586
+ } catch {
12587
+ return false;
12588
+ }
12406
12589
  }
12407
12590
 
12408
12591
  // src/commands/character.ts
@@ -12487,8 +12670,10 @@ async function runCharacter(opts) {
12487
12670
  log.plain(c.bold("Character quote"));
12488
12671
  log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
12489
12672
  if (price.controllerPackKey) {
12673
+ const version = price.controllerPackVersion === void 0 ? "" : ` v${price.controllerPackVersion}`;
12674
+ const fingerprint = price.controllerPackFingerprint ? ` \xB7 fingerprint ${price.controllerPackFingerprint}` : "";
12490
12675
  log.plain(
12491
- ` controller pack ${c.cyan(price.controllerPackKey)} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
12676
+ ` controller pack ${c.cyan(`${price.controllerPackKey}${version}`)}${fingerprint} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
12492
12677
  );
12493
12678
  }
12494
12679
  log.dim(` ${price.actionsGenerated} Meshy animation task${price.actionsGenerated === 1 ? "" : "s"} in this request.`);