@genex-ai/cli-demo 0.60.0 → 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
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- var RAW_CHANNEL = "latest";
11
+ var RAW_CHANNEL = "dev";
12
12
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
13
13
  var STANDS = {
14
14
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -1089,13 +1089,13 @@ function renderGenexConfig() {
1089
1089
  return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
1090
1090
  //
1091
1091
  // One build runs on BOTH stands: the serving hostname picks the stack at runtime
1092
- // (\`.auras.cc\` -> dev, else prod), so promoting a game is a copy, never a rebuild.
1092
+ // (\`-dev.genex.technology\` -> dev), so promoting a game is a copy, never a rebuild.
1093
1093
  // Vite env still overrides, but is loaded ONLY by \`npm run dev\`:
1094
1094
  // .env -> VITE_GENEX_SLUG (this game's identity; committed)
1095
1095
  // .env.development.local -> local-stack URL overrides (dev mode ONLY; gitignored)
1096
1096
  const IS_DEV =
1097
1097
  typeof location !== "undefined" &&
1098
- location.hostname.endsWith(".auras.cc");
1098
+ location.hostname.endsWith("-dev.genex.technology");
1099
1099
  export const GENEX = {
1100
1100
  slug: import.meta.env.VITE_GENEX_SLUG as string,
1101
1101
  apiUrl:
@@ -1711,6 +1711,9 @@ async function pushSource(cwd, ctx, log) {
1711
1711
  if (!fresh) return false;
1712
1712
  return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log);
1713
1713
  }
1714
+ function urlHasEmbeddedCredentials(pushUrl) {
1715
+ return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(pushUrl);
1716
+ }
1714
1717
  async function pushWorktree(cwd, pushUrl, managed, log) {
1715
1718
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1716
1719
  const failed = () => {
@@ -1719,6 +1722,11 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
1719
1722
  };
1720
1723
  const gitDir = await fs9.mkdtemp(path10.join(os4.tmpdir(), "genex-source-"));
1721
1724
  const base = { GIT_DIR: gitDir };
1725
+ if (urlHasEmbeddedCredentials(pushUrl)) {
1726
+ base.GIT_CONFIG_COUNT = "1";
1727
+ base.GIT_CONFIG_KEY_0 = "credential.helper";
1728
+ base.GIT_CONFIG_VALUE_0 = "";
1729
+ }
1722
1730
  const ident = {
1723
1731
  GIT_AUTHOR_NAME: "genex",
1724
1732
  GIT_AUTHOR_EMAIL: "agent@genex.local",
@@ -2747,656 +2755,62 @@ async function runWait(opts) {
2747
2755
  import fs12 from "fs/promises";
2748
2756
  import path13 from "path";
2749
2757
 
2750
- // src/lib/anims.ts
2751
- import fs11 from "fs/promises";
2752
- import path12 from "path";
2753
- var ANIMS_DEST = path12.join("public", "assets", "anims");
2754
- var HIDDEN_TAG = "reference";
2755
- async function runAnims(opts) {
2756
- const log = createLogger({ quiet: opts.quiet });
2757
- const root = opts.cwd ?? process.cwd();
2758
- const selectors = opts.selectors ?? [];
2759
- log.plain(c.bold("genex controller anims"));
2760
- log.plain("");
2761
- const { manifest, source } = await loadManifest(opts.animsBase);
2762
- if (source === "snapshot") {
2763
- log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
2764
- }
2765
- if (opts.list) {
2766
- printCatalog(log, manifest, selectors);
2767
- return;
2768
- }
2769
- const controllerMarker = path12.join(root, "src", "controllers", "character");
2770
- if (!await exists2(controllerMarker)) {
2771
- log.error(
2772
- `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
2773
- );
2774
- log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
2775
- process.exitCode = 1;
2776
- return;
2777
- }
2778
- const destDir = path12.join(root, ANIMS_DEST);
2779
- const gameManifestPath = path12.join(destDir, "manifest.json");
2780
- if (opts.reset) {
2781
- await fs11.rm(destDir, { recursive: true, force: true });
2782
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
2783
- }
2784
- if (selectors.length === 0) {
2785
- const installed = await readGameManifest(gameManifestPath);
2786
- if (installed === null || installed.clips.length === 0) {
2787
- log.plain(" No animation packs installed yet.");
2788
- } else {
2789
- log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
2790
- }
2791
- log.plain("");
2792
- log.plain(
2793
- ` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
2794
- "genex controller anims --list"
2795
- )}.`
2796
- );
2797
- return;
2798
- }
2799
- let resolved;
2800
- try {
2801
- resolved = resolveSelectors(manifest, selectors);
2802
- } catch (err) {
2803
- log.error(err instanceof Error ? err.message : String(err));
2804
- process.exitCode = 1;
2805
- return;
2806
- }
2807
- const coreNames = new Set(manifest.core);
2808
- const byName = /* @__PURE__ */ new Map();
2809
- let bundledSkips = 0;
2810
- for (const entries of resolved.values()) {
2811
- for (const entry of entries) {
2812
- if (coreNames.has(entry.name)) {
2813
- bundledSkips++;
2814
- continue;
2815
- }
2816
- byName.set(entry.name, entry);
2817
- }
2818
- }
2819
- const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2820
- const cacheDir = path12.join(
2821
- opts.cacheDir ?? getAnimsCacheDir(),
2822
- `${manifest.library}-v${manifest.version}`
2823
- );
2824
- await fs11.mkdir(cacheDir, { recursive: true });
2825
- await fs11.mkdir(destDir, { recursive: true });
2826
- const base = getAnimsBase(opts.animsBase);
2827
- let installedCount = 0;
2828
- let presentCount = 0;
2829
- let addedBytes = 0;
2830
- const failures = [];
2831
- for (const entry of wanted) {
2832
- const dest = path12.join(destDir, entry.file);
2833
- if (await hasSize(dest, entry.bytes)) {
2834
- presentCount++;
2835
- continue;
2836
- }
2837
- try {
2838
- const cached = path12.join(cacheDir, entry.file);
2839
- if (!await hasSize(cached, entry.bytes)) {
2840
- const res = await fetch(base + entry.file);
2841
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
2842
- const buf = Buffer.from(await res.arrayBuffer());
2843
- await fs11.writeFile(cached, buf);
2844
- }
2845
- await fs11.copyFile(cached, dest);
2846
- installedCount++;
2847
- addedBytes += entry.bytes;
2848
- log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
2849
- } catch (err) {
2850
- failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
2851
- }
2852
- }
2853
- const previous = await readGameManifest(gameManifestPath);
2854
- const union = new Set(previous?.clips ?? []);
2855
- for (const entry of wanted) {
2856
- if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
2857
- }
2858
- const gameManifest = {
2859
- schema: 1,
2860
- library: manifest.library,
2861
- version: manifest.version,
2862
- clips: [...union].sort((a, b) => a.localeCompare(b))
2863
- };
2864
- await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
2865
- log.plain("");
2866
- const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
2867
- if (presentCount > 0) parts.push(`${presentCount} already present`);
2868
- if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
2869
- log.success(
2870
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
2871
- );
2872
- for (const [selector, entries] of resolved) {
2873
- const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
2874
- if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
2875
- }
2876
- log.info(
2877
- `Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
2878
- );
2879
- if (failures.length > 0) {
2880
- log.plain("");
2881
- log.error(
2882
- `${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
2883
- );
2884
- log.plain(
2885
- " Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
2886
- );
2887
- process.exitCode = 1;
2888
- }
2889
- }
2890
- async function loadManifest(baseOverride) {
2891
- const base = getAnimsBase(baseOverride);
2892
- try {
2893
- const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
2894
- if (res.ok) {
2895
- const manifest2 = await res.json();
2896
- if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
2897
- return { manifest: manifest2, source: "cdn" };
2898
- }
2899
- }
2900
- } catch {
2901
- }
2902
- const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
2903
- const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
2904
- return { manifest, source: "snapshot" };
2905
- }
2906
- function resolveSelectors(manifest, selectors) {
2907
- const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
2908
- const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
2909
- const tags = /* @__PURE__ */ new Map();
2910
- for (const entry of manifest.clips) {
2911
- for (const tag of entry.tags) {
2912
- const list = tags.get(tag) ?? [];
2913
- list.push(entry);
2914
- tags.set(tag, list);
2915
- }
2916
- }
2917
- const out = /* @__PURE__ */ new Map();
2918
- for (const selector of selectors) {
2919
- const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
2920
- if (exact) {
2921
- out.set(selector, [exact]);
2922
- continue;
2923
- }
2924
- const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
2925
- if (tagHit) {
2926
- out.set(selector, tagHit);
2927
- continue;
2928
- }
2929
- const candidates = [...tags.keys(), ...byName.keys()];
2930
- const close = suggest(selector, candidates);
2931
- throw new Error(
2932
- `unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
2933
- "genex controller anims --list"
2934
- )} for the catalog.`
2935
- );
2936
- }
2937
- return out;
2938
- }
2939
- function suggest(input, candidates) {
2940
- const lower = input.toLowerCase();
2941
- const scored = [];
2942
- for (const candidate of candidates) {
2943
- const candidateLower = candidate.toLowerCase();
2944
- if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
2945
- scored.push({ name: candidate, score: 0 });
2946
- continue;
2947
- }
2948
- const distance = levenshtein(lower, candidateLower, 2);
2949
- if (distance <= 2) scored.push({ name: candidate, score: distance });
2950
- }
2951
- scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
2952
- return scored.slice(0, 4).map((s) => s.name);
2953
- }
2954
- function levenshtein(a, b, max) {
2955
- if (Math.abs(a.length - b.length) > max) return max + 1;
2956
- let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
2957
- for (let i = 1; i <= a.length; i++) {
2958
- const curr = [i];
2959
- let rowMin = i;
2960
- for (let j = 1; j <= b.length; j++) {
2961
- curr[j] = Math.min(
2962
- prev[j] + 1,
2963
- curr[j - 1] + 1,
2964
- prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2965
- );
2966
- if (curr[j] < rowMin) rowMin = curr[j];
2967
- }
2968
- if (rowMin > max) return max + 1;
2969
- prev = curr;
2970
- }
2971
- return prev[b.length];
2972
- }
2973
- function printCatalog(log, manifest, selectors) {
2974
- const coreNames = new Set(manifest.core);
2975
- if (selectors.length > 0) {
2976
- let resolved;
2977
- try {
2978
- resolved = resolveSelectors(manifest, selectors);
2979
- } catch (err) {
2980
- log.error(err instanceof Error ? err.message : String(err));
2981
- process.exitCode = 1;
2982
- return;
2983
- }
2984
- for (const [selector, entries] of resolved) {
2985
- log.plain(c.bold(selector));
2986
- for (const entry of entries) {
2987
- const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
2988
- log.plain(
2989
- ` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
2990
- );
2991
- }
2992
- }
2993
- return;
2994
- }
2995
- const tags = /* @__PURE__ */ new Map();
2996
- for (const entry of manifest.clips) {
2997
- for (const tag of entry.tags) {
2998
- if (tag === HIDDEN_TAG) continue;
2999
- const list = tags.get(tag) ?? [];
3000
- list.push(entry);
3001
- tags.set(tag, list);
3002
- }
3003
- }
3004
- log.plain(
3005
- `${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
3006
- );
3007
- log.plain(
3008
- ` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
3009
- "genex controller anims --list <tag>"
3010
- )}`
3011
- );
3012
- log.plain("");
3013
- for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
3014
- const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
3015
- const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
3016
- log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
3017
- }
3018
- }
3019
- async function readGameManifest(file) {
3020
- try {
3021
- return JSON.parse(await fs11.readFile(file, "utf8"));
3022
- } catch {
3023
- return null;
3024
- }
3025
- }
3026
- async function hasSize(file, bytes) {
3027
- try {
3028
- return (await fs11.stat(file)).size === bytes;
3029
- } catch {
3030
- return false;
3031
- }
3032
- }
3033
- async function exists2(p) {
3034
- try {
3035
- await fs11.access(p);
3036
- return true;
3037
- } catch {
3038
- return false;
3039
- }
3040
- }
3041
- function formatMb(bytes) {
3042
- return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
3043
- }
2758
+ // ../../packages/meshy-animation-catalog/src/index.ts
2759
+ import { createHash } from "crypto";
3044
2760
 
3045
- // src/commands/controller.ts
3046
- var CONTROLLER_KINDS = [
3047
- "character",
3048
- "car",
3049
- "drone",
3050
- "touch",
3051
- "networked-physics"
3052
- ];
3053
- var SHARED = [
3054
- "shared/math.ts",
3055
- "shared/physics-world.ts",
3056
- "shared/colliders.ts"
3057
- ];
3058
- var TOUCH_KIT = [
3059
- "touch/touch-joystick.ts",
3060
- "touch/drag-zone.ts",
3061
- "touch/rotate-overlay.ts"
3062
- ];
3063
- var INPUT_AND_CAMERA = [
3064
- "character/follow-camera.ts",
3065
- "character/keyboard-input.ts",
3066
- "character/touch-joystick.ts",
3067
- ...TOUCH_KIT
3068
- ];
3069
- var NOTICE = "NOTICE.md";
3070
- var CONTROLLER_FILE_SETS = {
3071
- character: {
3072
- code: [
3073
- ...SHARED,
3074
- "character/character-controller.ts",
3075
- "character/character-animations.ts",
3076
- "character/animation-packs.ts",
3077
- "character/motion-actions.ts",
3078
- "character/meshy/meshy-loader.ts",
3079
- "character/presets.ts",
3080
- // VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
3081
- // the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
3082
- "character/vrm/vrm-loader.ts",
3083
- "character/vrm/vrm-retarget.ts",
3084
- "character/vrm/capsule-fit.ts",
3085
- "character/vrm/foot-ik.ts",
3086
- ...INPUT_AND_CAMERA,
3087
- NOTICE
3088
- ],
3089
- // The player's VRM is written to public/assets/avatar.vrm at install time by
3090
- // installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
3091
- // a static manifest asset. animation-library.glb (the 12-clip core) still is;
3092
- // extra clips arrive via `genex controller anims` into public/assets/anims/.
3093
- assets: ["assets/animation-library.glb"],
3094
- skill: "genex-threejs-character-controller",
3095
- sketch: [
3096
- `const physics = await PhysicsWorld.create();`,
3097
- `const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
3098
- `const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
3099
- `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
3100
- `character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
3101
- `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
3102
- ]
2761
+ // ../../packages/meshy-animation-catalog/src/generated.ts
2762
+ var MESHY_ANIMATION_CATALOG = [
2763
+ {
2764
+ "actionId": -2,
2765
+ "key": "Walking_man",
2766
+ "name": "Walking",
2767
+ "category": "WalkAndRun",
2768
+ "subCategory": "Walking",
2769
+ "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking.gif",
2770
+ "rigType": "biped",
2771
+ "tag": null,
2772
+ "isDefault": true,
2773
+ "isFree": true,
2774
+ "createdAt": 1750829487798
3103
2775
  },
3104
- car: {
3105
- code: [
3106
- ...SHARED,
3107
- "vehicle/vehicle-controller.ts",
3108
- "vehicle/wheel.ts",
3109
- "vehicle/presets.ts",
3110
- "interact/enter-exit.ts",
3111
- ...INPUT_AND_CAMERA,
3112
- NOTICE
3113
- ],
3114
- assets: [],
3115
- skill: "genex-threejs-vehicle-controllers",
3116
- sketch: [
3117
- `const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
3118
- `const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
3119
- `scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
3120
- ]
2776
+ {
2777
+ "actionId": -1,
2778
+ "key": "Running",
2779
+ "name": "Running",
2780
+ "category": "WalkAndRun",
2781
+ "subCategory": "Running",
2782
+ "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Running.gif",
2783
+ "rigType": "biped",
2784
+ "tag": null,
2785
+ "isDefault": true,
2786
+ "isFree": true,
2787
+ "createdAt": 1750829487790
3121
2788
  },
3122
- drone: {
3123
- code: [
3124
- ...SHARED,
3125
- "drone/drone-controller.ts",
3126
- "drone/presets.ts",
3127
- "interact/enter-exit.ts",
3128
- ...INPUT_AND_CAMERA,
3129
- NOTICE
3130
- ],
3131
- assets: [],
3132
- skill: "genex-threejs-vehicle-controllers",
3133
- sketch: [
3134
- `const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
3135
- `const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
3136
- `physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
3137
- ]
2789
+ {
2790
+ "actionId": 0,
2791
+ "key": "Idle",
2792
+ "name": "Idle",
2793
+ "category": "DailyActions",
2794
+ "subCategory": "Idle",
2795
+ "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Idle.gif",
2796
+ "rigType": "style_01",
2797
+ "tag": null,
2798
+ "isDefault": false,
2799
+ "isFree": true,
2800
+ "createdAt": 1750829487810
3138
2801
  },
3139
- touch: {
3140
- code: [...TOUCH_KIT, NOTICE],
3141
- assets: [],
3142
- skill: "genex-threejs-touch-controls",
3143
- sketch: [
3144
- `const joy = new TouchJoystick({ floating: true }); // safe-area-aware defaults; static circle without the flag`,
3145
- `const jump = new VirtualButton({ label: "Jump", onPress: () => player.jump() });`,
3146
- `const look = new DragZone(); // right half; per frame: const { dx, dy } = look.consumeDelta()`,
3147
- `[joy, jump, look].forEach((w) => w.setVisible(navigator.maxTouchPoints > 0));`
3148
- ]
3149
- },
3150
- "networked-physics": {
3151
- code: [
3152
- ...SHARED,
3153
- "network/pose.ts",
3154
- "network/networked-pushable.ts",
3155
- "network/networked-vehicle.ts",
3156
- "NETWORKING.md",
3157
- NOTICE
3158
- ],
3159
- assets: [],
3160
- skill: "genex-threejs-multiplayer",
3161
- sketch: [
3162
- `const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
3163
- `physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
3164
- `contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
3165
- ]
3166
- }
3167
- };
3168
- var CODE_DEST = path13.join("src", "controllers");
3169
- var ASSETS_DEST = path13.join("public", "assets");
3170
- async function runController(opts) {
3171
- const log = createLogger({ quiet: opts.quiet });
3172
- if (opts.kind?.trim() === "anims") {
3173
- await runAnims(opts);
3174
- return;
3175
- }
3176
- const kind = opts.kind?.trim();
3177
- if (!kind || !CONTROLLER_KINDS.includes(kind)) {
3178
- log.error(
3179
- `Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
3180
- "genex controller <character|car|drone|touch|networked-physics> [--force]"
3181
- )} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
3182
- );
3183
- process.exitCode = 1;
3184
- return;
3185
- }
3186
- const srcDir = path13.join(getTemplatesDir(), "controllers");
3187
- const root = opts.cwd ?? process.cwd();
3188
- const set = CONTROLLER_FILE_SETS[kind];
3189
- log.plain(c.bold(`genex controller ${kind}`));
3190
- log.plain("");
3191
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
3192
- const plan = [
3193
- ...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
3194
- ...set.assets.map((rel) => ({
3195
- from: rel,
3196
- rel: path13.join(ASSETS_DEST, path13.basename(rel))
3197
- }))
3198
- ];
3199
- let copied = 0;
3200
- let skipped = 0;
3201
- try {
3202
- for (const file of plan) {
3203
- const dest = path13.join(root, file.rel);
3204
- if (!opts.force && await exists3(dest)) {
3205
- skipped++;
3206
- log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
3207
- continue;
3208
- }
3209
- await fs12.mkdir(path13.dirname(dest), { recursive: true });
3210
- await fs12.copyFile(path13.join(srcDir, file.from), dest);
3211
- copied++;
3212
- log.dim(` ${file.rel}`);
3213
- }
3214
- } catch (err) {
3215
- log.error(`Copy failed: ${String(err)}`);
3216
- process.exitCode = 1;
3217
- return;
3218
- }
3219
- log.success(
3220
- `Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
3221
- );
3222
- log.plain("");
3223
- if (kind === "character") {
3224
- if (opts.character) {
3225
- try {
3226
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
3227
- if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
3228
- await installMeshyCharacterManifest({
3229
- root,
3230
- characterId: opts.character,
3231
- apiUrl: getApiUrl(opts.apiUrl),
3232
- token,
3233
- log
3234
- });
3235
- } catch (error) {
3236
- log.error(error instanceof Error ? error.message : String(error));
3237
- process.exitCode = 1;
3238
- return;
3239
- }
3240
- } else {
3241
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
3242
- await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
3243
- }
3244
- log.plain("");
3245
- }
3246
- log.plain(c.bold("Next steps"));
3247
- if (kind !== "touch") {
3248
- log.plain(
3249
- ` 1. ${c.cyan(
3250
- 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"
3251
- )} (three is already in the scaffold).`
3252
- );
3253
- }
3254
- const stepOffset = kind === "touch" ? 0 : 1;
3255
- log.plain(
3256
- ` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
3257
- );
3258
- log.plain(
3259
- kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
3260
- );
3261
- const sketch = kind === "character" && opts.character ? [
3262
- `const physics = await PhysicsWorld.create();`,
3263
- `const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
3264
- `const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
3265
- `character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
3266
- `physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
3267
- ] : set.sketch;
3268
- for (const line of sketch) {
3269
- log.dim(` ${line}`);
3270
- }
3271
- await firstPreviewNudge(log);
3272
- }
3273
- async function installMeshyCharacterManifest(args) {
3274
- const response = await apiFetch(
3275
- `${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
3276
- { headers: { Authorization: `Bearer ${args.token}` } }
3277
- );
3278
- if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
3279
- if (!response.ok) throw new Error(`Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
3280
- const body = await response.json();
3281
- const manifest = body.manifest;
3282
- if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId) {
3283
- throw new Error("The API returned an invalid Meshy character manifest.");
3284
- }
3285
- const destination = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
3286
- await fs12.mkdir(path13.dirname(destination), { recursive: true });
3287
- await fs12.writeFile(
3288
- destination,
3289
- `${JSON.stringify(manifest, null, 2)}
3290
- `
3291
- );
3292
- args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
3293
- const pack = manifest.controllerPack;
3294
- if (typeof pack?.key === "string" && typeof pack.version === "number") {
3295
- args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
3296
- } else {
3297
- args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the preview-reviewed neutral-v2 locomotion pack.");
3298
- }
3299
- const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
3300
- args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
3301
- const slots = Object.keys(manifest.locomotion?.bindings ?? manifest.locomotion?.slots ?? {}).sort();
3302
- args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
3303
- const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
3304
- if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
3305
- else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
3306
- }
3307
- async function installOwnerAvatar(args) {
3308
- const { root, srcDir, apiUrl, token, log } = args;
3309
- const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
3310
- await fs12.mkdir(path13.dirname(dest), { recursive: true });
3311
- if (token) {
3312
- try {
3313
- const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
3314
- headers: { Authorization: `Bearer ${token}` }
3315
- });
3316
- if (res.ok) {
3317
- const me = await res.json();
3318
- if (me.vrmUrl) {
3319
- const vrmRes = await fetch(me.vrmUrl);
3320
- if (vrmRes.ok) {
3321
- const buf = Buffer.from(await vrmRes.arrayBuffer());
3322
- await fs12.writeFile(dest, buf);
3323
- log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
3324
- return;
3325
- }
3326
- }
3327
- }
3328
- log.dim(" couldn't fetch your avatar; using the bundled default.");
3329
- } catch {
3330
- log.dim(" avatar fetch failed (offline?); using the bundled default.");
3331
- }
3332
- }
3333
- await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
3334
- log.dim(
3335
- token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
3336
- );
3337
- }
3338
- async function exists3(p) {
3339
- try {
3340
- await fs12.access(p);
3341
- return true;
3342
- } catch {
3343
- return false;
3344
- }
3345
- }
3346
-
3347
- // ../../packages/meshy-animation-catalog/src/generated.ts
3348
- var MESHY_ANIMATION_CATALOG = [
3349
- {
3350
- "actionId": -2,
3351
- "key": "Walking_man",
3352
- "name": "Walking",
3353
- "category": "WalkAndRun",
3354
- "subCategory": "Walking",
3355
- "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking.gif",
3356
- "rigType": "biped",
3357
- "tag": null,
3358
- "isDefault": true,
3359
- "isFree": true,
3360
- "createdAt": 1750829487798
3361
- },
3362
- {
3363
- "actionId": -1,
3364
- "key": "Running",
3365
- "name": "Running",
3366
- "category": "WalkAndRun",
3367
- "subCategory": "Running",
3368
- "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Running.gif",
3369
- "rigType": "biped",
3370
- "tag": null,
3371
- "isDefault": true,
3372
- "isFree": true,
3373
- "createdAt": 1750829487790
3374
- },
3375
- {
3376
- "actionId": 0,
3377
- "key": "Idle",
3378
- "name": "Idle",
3379
- "category": "DailyActions",
3380
- "subCategory": "Idle",
3381
- "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Idle.gif",
3382
- "rigType": "style_01",
3383
- "tag": null,
3384
- "isDefault": false,
3385
- "isFree": true,
3386
- "createdAt": 1750829487810
3387
- },
3388
- {
3389
- "actionId": 1,
3390
- "key": "Walking_Woman",
3391
- "name": "Walking Woman",
3392
- "category": "WalkAndRun",
3393
- "subCategory": "Walking",
3394
- "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking_Woman_woman.gif",
3395
- "rigType": "style_01",
3396
- "tag": null,
3397
- "isDefault": false,
3398
- "isFree": true,
3399
- "createdAt": 1750829487808
2802
+ {
2803
+ "actionId": 1,
2804
+ "key": "Walking_Woman",
2805
+ "name": "Walking Woman",
2806
+ "category": "WalkAndRun",
2807
+ "subCategory": "Walking",
2808
+ "previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking_Woman_woman.gif",
2809
+ "rigType": "style_01",
2810
+ "tag": null,
2811
+ "isDefault": false,
2812
+ "isFree": true,
2813
+ "createdAt": 1750829487808
3400
2814
  },
3401
2815
  {
3402
2816
  "actionId": 2,
@@ -12217,6 +11631,12 @@ var CURATED = {
12217
11631
  controllerSlots: ["crouch.forward"],
12218
11632
  reviewStatus: "preview-reviewed"
12219
11633
  },
11634
+ [657]: {
11635
+ loop: true,
11636
+ motionPolicy: "controller-loop",
11637
+ controllerSlots: ["run.forward"],
11638
+ reviewStatus: "preview-reviewed"
11639
+ },
12220
11640
  [658]: {
12221
11641
  loop: true,
12222
11642
  motionPolicy: "controller-loop",
@@ -12226,169 +11646,946 @@ var CURATED = {
12226
11646
  [659]: {
12227
11647
  loop: true,
12228
11648
  motionPolicy: "controller-loop",
12229
- controllerSlots: ["run.forward"],
12230
- reviewStatus: "preview-reviewed"
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}`);
12231
12152
  }
12232
- };
12233
- var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
12234
- var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
12235
- "Climbing",
12236
- "HangingfromLedge",
12237
- "VaultingOverObstacle",
12238
- "Interacting",
12239
- "PickingUpItem",
12240
- "Pushing",
12241
- "Sleeping"
12242
- ]);
12243
- var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
12244
- var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
12245
- function words(value) {
12246
- return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
12247
12153
  }
12248
- function inferRequirements(key, subCategory) {
12249
- const lower = `${key} ${subCategory}`.toLowerCase().replace(/[^a-z0-9]+/g, " ");
12250
- const props = [
12251
- ["gun", /\b(?:gun|rifle|pistol)\b/],
12252
- ["sword", /\b(?:sword|blade)\b/],
12253
- ["bow", /\b(?:bow|arrow)\b/],
12254
- ["shield", /\bshield\b/],
12255
- ["chair", /\b(?:chair|sit|sitting)\b/],
12256
- ["ladder", /\bladder\b/],
12257
- ["rope", /\brope\b/]
12258
- ].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
12259
- const environment = [
12260
- ["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
12261
- ["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
12262
- ["water", /\b(?:swim|swimming)\b/],
12263
- ["seat", /\b(?:sit|sitting|chair)\b/]
12264
- ].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
12265
- 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
+ }
12266
12160
  }
12267
- function inferSlots(key) {
12268
- const lower = key.toLowerCase();
12269
- const band = lower.includes("crouch") ? "crouch" : lower.includes("run") || lower.includes("sprint") ? "run" : lower.includes("walk") ? "walk" : null;
12270
- if (!band) return [];
12271
- 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";
12272
- 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
+ }
12273
12167
  }
12274
- function curateMeshyAnimation(raw) {
12275
- const inPlace = raw.tag === "InPlace";
12276
- const requirements = inferRequirements(raw.key, raw.subCategory);
12277
- const choreography = CHOREOGRAPHY_SUBCATEGORIES.has(raw.subCategory) || requirements.partner;
12278
- const loop = inPlace && LOOP_SUBCATEGORIES.has(raw.subCategory) && !NON_LOOP_ACTION.test(`${raw.key} ${raw.name}`);
12279
- const motionPolicy = loop ? "controller-loop" : choreography ? "choreography" : PLANAR_ACTION.test(`${raw.key} ${raw.name}`) ? "planar-root-action" : "anchored-action";
12280
- const base = {
12281
- actionId: raw.actionId,
12282
- key: raw.key,
12283
- name: raw.name,
12284
- category: raw.category,
12285
- subCategory: raw.subCategory,
12286
- previewUrl: raw.previewUrl,
12287
- rigType: raw.rigType,
12288
- inPlace,
12289
- isDefault: raw.isDefault,
12290
- isFree: raw.isFree,
12291
- createdAt: raw.createdAt,
12292
- aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
12293
- gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
12294
- loop,
12295
- motionPolicy,
12296
- rootMotionValidated: false,
12297
- // A name that contains "walk" or "run" is not enough to make a safe
12298
- // controller loop. Only provider-declared InPlace loops (plus explicit
12299
- // measured overrides above) may populate automatic locomotion slots.
12300
- controllerSlots: loop ? inferSlots(raw.key) : [],
12301
- requirements,
12302
- reviewStatus: "metadata-reviewed"
12303
- };
12304
- 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`;
12305
12178
  }
12306
12179
 
12307
- // ../../packages/meshy-animation-catalog/src/index.ts
12308
- var SYNONYMS = {
12309
- attack: ["fight", "punch", "kick", "weapon", "combat"],
12310
- combat: ["fight", "attack", "punch", "weapon"],
12311
- crouch: ["sneak", "stealth"],
12312
- die: ["death", "dying", "fall"],
12313
- emote: ["gesture", "acting", "dance"],
12314
- gun: ["rifle", "pistol", "shoot", "firearm"],
12315
- idle: ["stand", "breathing"],
12316
- jump: ["leap", "vault"],
12317
- run: ["running", "jog", "sprint", "charge"],
12318
- sit: ["sitting", "chair", "seat"],
12319
- skate: ["skateboard", "skating", "board"],
12320
- sword: ["blade", "weapon", "slash"],
12321
- walk: ["walking", "stride", "stroll"],
12322
- 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
+ }
12323
12302
  };
12324
- var STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "and", "for", "of", "the", "to", "with"]);
12325
- function tokens(value) {
12326
- return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((token) => token && !STOP_WORDS.has(token));
12327
- }
12328
- function tokenGroup(token) {
12329
- const group = /* @__PURE__ */ new Set([token, ...SYNONYMS[token] ?? []]);
12330
- for (const [canonical, synonyms] of Object.entries(SYNONYMS)) {
12331
- if (synonyms.includes(token)) {
12332
- group.add(canonical);
12333
- 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}`);
12334
12348
  }
12349
+ } catch (err) {
12350
+ log.error(`Copy failed: ${String(err)}`);
12351
+ process.exitCode = 1;
12352
+ return;
12335
12353
  }
12336
- return group;
12337
- }
12338
- var MESHY_ANIMATIONS = MESHY_ANIMATION_CATALOG.map(curateMeshyAnimation);
12339
- var MESHY_ANIMATIONS_BY_ID = new Map(MESHY_ANIMATIONS.map((entry) => [entry.actionId, entry]));
12340
- var MESHY_ANIMATIONS_BY_KEY = new Map(MESHY_ANIMATIONS.map((entry) => [entry.key.toLowerCase(), entry]));
12341
- function animationById(actionId) {
12342
- return MESHY_ANIMATIONS_BY_ID.get(actionId);
12343
- }
12344
- function searchMeshyAnimations(query, options = {}) {
12345
- const rawQuery = query.trim().toLowerCase();
12346
- const queryTokens = tokens(query);
12347
- const groups = queryTokens.map(tokenGroup);
12348
- const expanded = new Set(groups.flatMap((group) => [...group]));
12349
- const results = [];
12350
- for (const entry of MESHY_ANIMATIONS) {
12351
- if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
12352
- if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
12353
- const matched = [];
12354
- let score = 0;
12355
- if (String(entry.actionId) === rawQuery) {
12356
- score += 1e5;
12357
- 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 });
12358
12378
  }
12359
- if (entry.key.toLowerCase() === rawQuery) {
12360
- score += 8e4;
12361
- 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 {
12362
12421
  }
12363
- if (entry.name.toLowerCase() === rawQuery) {
12364
- score += 6e4;
12365
- 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
+ );
12366
12452
  }
12367
- const haystack = new Set(tokens([
12368
- entry.key,
12369
- entry.name,
12370
- entry.category,
12371
- entry.subCategory,
12372
- ...entry.aliases,
12373
- ...entry.gameplayTags
12374
- ].join(" ")));
12375
- let coveredGroups = 0;
12376
- for (let index = 0; index < groups.length; index++) {
12377
- const hits = [...groups[index]].filter((token) => haystack.has(token));
12378
- if (hits.length === 0) continue;
12379
- coveredGroups++;
12380
- const original = queryTokens[index];
12381
- const best = hits.includes(original) ? original : hits[0];
12382
- score += best === original ? 400 : 140;
12383
- 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
+ }
12384
12465
  }
12385
- if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score += 35;
12386
- const exact = score >= 6e4;
12387
- if (score > 0 && (exact || coveredGroups === groups.length)) {
12388
- 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.");
12389
12575
  }
12390
12576
  }
12391
- 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
+ }
12392
12589
  }
12393
12590
 
12394
12591
  // src/commands/character.ts
@@ -12473,8 +12670,10 @@ async function runCharacter(opts) {
12473
12670
  log.plain(c.bold("Character quote"));
12474
12671
  log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
12475
12672
  if (price.controllerPackKey) {
12673
+ const version = price.controllerPackVersion === void 0 ? "" : ` v${price.controllerPackVersion}`;
12674
+ const fingerprint = price.controllerPackFingerprint ? ` \xB7 fingerprint ${price.controllerPackFingerprint}` : "";
12476
12675
  log.plain(
12477
- ` 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(", ")}`
12478
12677
  );
12479
12678
  }
12480
12679
  log.dim(` ${price.actionsGenerated} Meshy animation task${price.actionsGenerated === 1 ? "" : "s"} in this request.`);