@genex-ai/cli-demo 1.34.0-dev.693 → 1.34.1-dev.694
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 +342 -253
- package/package.json +1 -1
- package/templates/skills/genex-ai-character/SKILL.md +6 -0
- package/templates/skills/genex-ai-music/SKILL.md +16 -19
- package/templates/skills/genex-ai-music/references/genex-audio-gesture.md +47 -0
- package/templates/skills/genex-ai-sfx/SKILL.md +14 -3
- package/templates/skills/genex-ai-voice/SKILL.md +15 -4
- package/templates/skills/genex-game-director/SKILL.md +9 -21
- package/templates/skills/genex-game-director/references/design-contract.md +7 -6
- package/templates/skills/genex-game-director/references/routing-map.md +7 -9
- package/templates/skills/genex-lane-card/SKILL.md +3 -3
- package/templates/skills/genex-threejs-exposure-color-grading/SKILL.md +10 -1
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +7 -1
- package/templates/skills/genex-threejs-visual-validation/references/genex-audio-output.md +43 -0
package/dist/index.js
CHANGED
|
@@ -864,13 +864,13 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
864
864
|
13. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\`, with \`<dashboard>\` from \`.genex/project.json\`) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. That page shows the build you just previewed; \`preview\` never disturbs the build players are on. When the player is happy with it, \`npx genex promote\` makes that exact build live for everyone \u2014 no rebuild. After every round of player feedback, end with a preview push. After the first release, the game is two versions and the player only ever hears these two words for them: the **draft** (their working copy, updated by \`preview\`) and the **public version** (what everyone plays). Command names are yours, not theirs \u2014 never say "promote" to the player; ask directly: "Want me to update the public version?", one line, once per round, and keep building while you wait \u2014 updating the public version is the ONE action that waits for an explicit yes, and more change requests instead of a yes mean "not yet". The player's "publish it" / "publish the update" / "update it" / "yes" after the first release ALL mean \`genex promote\` \u2014 never \`genex publish\` again, which would ship an untried rebuild. \`promote\` ships the last previewed build, so if anything was edited since the last \`preview\`, preview again before promoting.
|
|
865
865
|
14. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files. When the player asks for parallel work, repeated refinement passes, a critic reviewing your output, or names any way of working your platform provides, that IS your instruction \u2014 wherever it reaches you, including inside their answer to a question you asked \u2014 so adopt it as the working mode for the rest of the build, starting with the work in front of you, and never file it as a later milestone. While delegated work runs, keep building or talk to the player; never idle in a foreground wait for something your platform will tell you about.
|
|
866
866
|
15. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
|
|
867
|
-
16. Never add debug-only code
|
|
867
|
+
16. Never add debug-only code that fakes gameplay \u2014 no hidden test modes, special URL parameters, forced-visible flags or auth mocks. The visual-validation skill permits a development-only final-mix analyser for a deliberately triggered audible event; remove it from published builds. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
|
|
868
868
|
17. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
|
|
869
869
|
18. NEVER delete, empty, move, rename, or overwrite anything you did not create yourself. This folder may hold the player's own reference images, notes, sketches, or an earlier attempt \u2014 files that exist nowhere else and have no undo, no trash, no backup. A non-empty folder is normal and is NEVER something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck interactive prompt, and every setup tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files") \u2014 scaffold into a fresh subfolder and copy in instead. You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes. This binds hardest during setup, where it runs fast and automatically before the player has asked for anything at all.
|
|
870
870
|
19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
|
|
871
871
|
20. When the player names a specific thing to integrate \u2014 a library, a controller, an asset, a part of an existing game, or a tagged reference the platform placed in \`.genex/refs/<slug>/\` \u2014 integrate the real thing: read its source first and build from it, never a lighter look-alike written from scratch because it was faster. A tagged reference owns the subsystem it was tagged for, outranking the default lane and the vendored controllers for that subsystem. If you genuinely can't use it, say so in one plain line and ask how to proceed.
|
|
872
872
|
21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
|
|
873
|
-
22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
|
|
873
|
+
22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Start with one reusable implementation per distinct required gameplay or visual role. Background populations default to reusable procedural bodies and motion or compatible existing rigged assets. Catalog clips need a compatible skeleton and may be paid. Reserve paid custom bodies for the player and characters examined or interacted with closely, while honoring explicit user requests. Add variants only for an unmet requirement; unspent allowance is not unfinished work. Allowance questions use timeoutPolicy no-consent: silence does not raise the allowance. Record role, reuse and completion criteria in the existing Assets table. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
|
|
874
874
|
23. **The screenshots you take to look at your own work go in \`.genex/scratch/\`.** Captures, render comparisons, before/after strips, traces and metrics dumps are how you SEE what you built \u2014 they are not part of what you built, and \`genex preview\` pushes the whole folder to the game's source, every time, forever. \`.genex/scratch/\` already exists for this and is already ignored, so there is nothing to set up: write the capture as \`.genex/scratch/arena-before.png\` and read it back from there. Never put them in a folder of your own at the top level: \`progress/\`, \`reports/\`, \`shots/\` and their kind are pushed like source and become permanent weight in the game and in every remix of it \u2014 one real game reached 11 GB and 10,332 committed screenshots exactly this way. If you find such a folder already there, add it to \`.gitignore\`; never delete the player's files (law 18).
|
|
875
875
|
${CONTRACT_END}
|
|
876
876
|
`;
|
|
@@ -1897,6 +1897,30 @@ import "child_process";
|
|
|
1897
1897
|
import fs11 from "fs/promises";
|
|
1898
1898
|
import path11 from "path";
|
|
1899
1899
|
var ledgerPath = (cwd) => path11.join(cwd, ".genex", "generations.ndjson");
|
|
1900
|
+
async function appendAdmissionRow(cwd, event) {
|
|
1901
|
+
const file = await fs11.open(ledgerPath(cwd), "a");
|
|
1902
|
+
try {
|
|
1903
|
+
await file.writeFile(`${JSON.stringify(event)}
|
|
1904
|
+
`, "utf8");
|
|
1905
|
+
await file.sync();
|
|
1906
|
+
} finally {
|
|
1907
|
+
await file.close();
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
async function readAdmissionRows(cwd) {
|
|
1911
|
+
let raw;
|
|
1912
|
+
try {
|
|
1913
|
+
raw = await fs11.readFile(ledgerPath(cwd), "utf8");
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
if (error.code === "ENOENT") return [];
|
|
1916
|
+
throw error;
|
|
1917
|
+
}
|
|
1918
|
+
return raw.split("\n").filter((line) => line.trim()).map((line) => {
|
|
1919
|
+
const row = JSON.parse(line);
|
|
1920
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("Invalid generation ledger; reconcile it before spending.");
|
|
1921
|
+
return row;
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1900
1924
|
async function append(cwd, event) {
|
|
1901
1925
|
try {
|
|
1902
1926
|
await fs11.access(path11.join(cwd, ".genex"));
|
|
@@ -1941,19 +1965,6 @@ async function readLedger(cwd = process.cwd()) {
|
|
|
1941
1965
|
}
|
|
1942
1966
|
return [...entries.values()];
|
|
1943
1967
|
}
|
|
1944
|
-
async function recordQueued(id, kind, prompt, cwd = process.cwd(), extra = {}) {
|
|
1945
|
-
const credits = extra.credits;
|
|
1946
|
-
await append(cwd, {
|
|
1947
|
-
t: "q",
|
|
1948
|
-
id,
|
|
1949
|
-
kind,
|
|
1950
|
-
prompt: prompt.slice(0, 120),
|
|
1951
|
-
...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
|
|
1952
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1953
|
-
...typeof credits === "number" && Number.isFinite(credits) && credits >= 0 ? { credits } : {},
|
|
1954
|
-
...typeof extra.outDir === "string" && extra.outDir.trim() ? { out: extra.outDir.trim() } : {}
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
1968
|
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
1958
1969
|
const existing = (await readLedger(cwd)).find((e) => e.id === id);
|
|
1959
1970
|
if (existing && existing.status === status) return;
|
|
@@ -7884,8 +7895,8 @@ async function runRollback(opts) {
|
|
|
7884
7895
|
}
|
|
7885
7896
|
|
|
7886
7897
|
// src/commands/generate.ts
|
|
7887
|
-
import
|
|
7888
|
-
import
|
|
7898
|
+
import fs25 from "fs/promises";
|
|
7899
|
+
import path24 from "path";
|
|
7889
7900
|
import { PNG as PNG4 } from "pngjs";
|
|
7890
7901
|
|
|
7891
7902
|
// src/lib/glass.ts
|
|
@@ -8066,6 +8077,11 @@ function ceilingVerdict(input) {
|
|
|
8066
8077
|
return { allow: true };
|
|
8067
8078
|
}
|
|
8068
8079
|
|
|
8080
|
+
// src/lib/generation-admission.ts
|
|
8081
|
+
import fs24 from "fs/promises";
|
|
8082
|
+
import path23 from "path";
|
|
8083
|
+
import { createHash, randomUUID } from "crypto";
|
|
8084
|
+
|
|
8069
8085
|
// src/lib/asset-budget.ts
|
|
8070
8086
|
import fs23 from "fs/promises";
|
|
8071
8087
|
import path22 from "path";
|
|
@@ -8128,6 +8144,12 @@ async function writeBudgetStore(cwd, store) {
|
|
|
8128
8144
|
async function ensureBudgetStore(cwd, input) {
|
|
8129
8145
|
const existing = await readBudgetStore(cwd);
|
|
8130
8146
|
if (existing) return existing;
|
|
8147
|
+
try {
|
|
8148
|
+
await fs23.stat(storePath(cwd));
|
|
8149
|
+
throw new Error("Stored asset allowance is unreadable or invalid; reconcile it before paid generation.");
|
|
8150
|
+
} catch (error) {
|
|
8151
|
+
if (error.code !== "ENOENT") throw error;
|
|
8152
|
+
}
|
|
8131
8153
|
const store = {
|
|
8132
8154
|
v: 1,
|
|
8133
8155
|
allowanceCredits: allowanceFor({ spendable: input.spendable, config: input.config }),
|
|
@@ -8136,7 +8158,7 @@ async function ensureBudgetStore(cwd, input) {
|
|
|
8136
8158
|
spendableAtStart: input.spendable,
|
|
8137
8159
|
balanceAtStart: input.balance
|
|
8138
8160
|
};
|
|
8139
|
-
await writeBudgetStore(cwd, store);
|
|
8161
|
+
if (!await writeBudgetStore(cwd, store)) throw new Error("Cannot persist this build allowance; no paid generation was sent.");
|
|
8140
8162
|
return store;
|
|
8141
8163
|
}
|
|
8142
8164
|
function isFailureRow(row) {
|
|
@@ -8144,9 +8166,16 @@ function isFailureRow(row) {
|
|
|
8144
8166
|
}
|
|
8145
8167
|
function spentCredits(rows) {
|
|
8146
8168
|
const quoted = /* @__PURE__ */ new Map();
|
|
8169
|
+
const reservations = /* @__PURE__ */ new Map();
|
|
8147
8170
|
for (const row of rows) {
|
|
8148
8171
|
if (typeof row.id !== "string") continue;
|
|
8149
|
-
if (row.t === "
|
|
8172
|
+
if (row.t === "reserve") {
|
|
8173
|
+
if (typeof row.credits !== "number" || !Number.isFinite(row.credits) || row.credits < 0) throw new Error("Invalid allowance reservation");
|
|
8174
|
+
reservations.set(row.id, row.credits);
|
|
8175
|
+
} else if (row.t === "release") {
|
|
8176
|
+
reservations.delete(row.id);
|
|
8177
|
+
} else if (row.t === "q") {
|
|
8178
|
+
if (typeof row.reservationId === "string") reservations.delete(row.reservationId);
|
|
8150
8179
|
if (quoted.has(row.id)) continue;
|
|
8151
8180
|
const credits = typeof row.credits === "number" && Number.isFinite(row.credits) && row.credits > 0 ? row.credits : 0;
|
|
8152
8181
|
quoted.set(row.id, credits);
|
|
@@ -8156,6 +8185,7 @@ function spentCredits(rows) {
|
|
|
8156
8185
|
}
|
|
8157
8186
|
let total = 0;
|
|
8158
8187
|
for (const credits of quoted.values()) total += credits;
|
|
8188
|
+
for (const credits of reservations.values()) total += credits;
|
|
8159
8189
|
return total;
|
|
8160
8190
|
}
|
|
8161
8191
|
function budgetVerdict(input) {
|
|
@@ -8197,28 +8227,156 @@ async function fetchCreditsSnapshot(apiUrl, token) {
|
|
|
8197
8227
|
}
|
|
8198
8228
|
async function assetBudgetGate(input) {
|
|
8199
8229
|
const config = input.config ?? budgetConfig();
|
|
8200
|
-
if (budgetDisabled(config)) return { allow: true };
|
|
8230
|
+
if (input.quotedCredits === 0 || budgetDisabled(config)) return { allow: true };
|
|
8201
8231
|
const cwd = input.cwd ?? process.cwd();
|
|
8202
8232
|
const snapshot = await fetchCreditsSnapshot(input.apiUrl, input.token);
|
|
8203
8233
|
if (!snapshot) {
|
|
8204
|
-
|
|
8205
|
-
return { allow: true };
|
|
8234
|
+
return { allow: false, reason: "Cannot read the wallet to check the asset allowance. No paid generation was sent; retry when the API is available." };
|
|
8206
8235
|
}
|
|
8207
8236
|
if (snapshot.unlimited) return { allow: true };
|
|
8208
8237
|
const price = typeof input.quotedCredits === "number" && Number.isFinite(input.quotedCredits) ? input.quotedCredits : snapshot.prices[input.kind];
|
|
8209
8238
|
if (typeof price !== "number" || !Number.isFinite(price)) {
|
|
8210
|
-
|
|
8211
|
-
return { allow: true };
|
|
8239
|
+
return { allow: false, reason: `No valid live quote for ${input.kind}; no generation was sent.` };
|
|
8212
8240
|
}
|
|
8213
8241
|
const store = await ensureBudgetStore(cwd, {
|
|
8214
8242
|
balance: snapshot.balance,
|
|
8215
8243
|
spendable: snapshot.spendable,
|
|
8216
8244
|
config
|
|
8217
8245
|
});
|
|
8218
|
-
const spent = spentCredits(await
|
|
8246
|
+
const spent = spentCredits(await readAdmissionRows(cwd));
|
|
8219
8247
|
return budgetVerdict({ kind: input.kind, price, spent, allowance: store.allowanceCredits, config });
|
|
8220
8248
|
}
|
|
8221
8249
|
|
|
8250
|
+
// src/lib/generation-admission.ts
|
|
8251
|
+
async function withAdmissionLock(cwd, work, waitMs = 3e4) {
|
|
8252
|
+
const lock = path23.join(cwd, ".genex", "generation-admission.lock");
|
|
8253
|
+
const deadline = Date.now() + waitMs;
|
|
8254
|
+
let file;
|
|
8255
|
+
while (!file) {
|
|
8256
|
+
try {
|
|
8257
|
+
file = await fs24.open(lock, "wx");
|
|
8258
|
+
} catch (error) {
|
|
8259
|
+
if (error.code !== "EEXIST") throw error;
|
|
8260
|
+
if (Date.now() >= deadline) throw new Error("Another generation admission holds .genex/generation-admission.lock. If its process died, reconcile pending reservations before removing that lock.");
|
|
8261
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
try {
|
|
8265
|
+
await file.writeFile(String(process.pid));
|
|
8266
|
+
return await work();
|
|
8267
|
+
} finally {
|
|
8268
|
+
await file.close();
|
|
8269
|
+
await fs24.unlink(lock);
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
async function enqueueWithAllowance(input) {
|
|
8273
|
+
const cwd = input.cwd ?? process.cwd();
|
|
8274
|
+
try {
|
|
8275
|
+
return await withAdmissionLock(cwd, async () => {
|
|
8276
|
+
const createPath = input.createPath ?? "/api/generations";
|
|
8277
|
+
const generic = createPath === "/api/generations";
|
|
8278
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${input.token}` };
|
|
8279
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([input.apiUrl, createPath, input.body])).digest("hex");
|
|
8280
|
+
const rows = await readAdmissionRows(cwd);
|
|
8281
|
+
const pending = /* @__PURE__ */ new Map();
|
|
8282
|
+
for (const row of rows) {
|
|
8283
|
+
if (row.t === "reserve" && typeof row.id === "string") pending.set(row.id, row);
|
|
8284
|
+
if (row.t === "release" && typeof row.id === "string") pending.delete(row.id);
|
|
8285
|
+
if (row.t === "q" && typeof row.reservationId === "string") pending.delete(row.reservationId);
|
|
8286
|
+
}
|
|
8287
|
+
for (const [requestId2, row] of pending) {
|
|
8288
|
+
if (row.apiUrl === input.apiUrl && row.generic === true) {
|
|
8289
|
+
const response2 = await apiFetch(`${input.apiUrl}/api/generations/requests/${encodeURIComponent(requestId2)}`, { headers });
|
|
8290
|
+
if (response2.ok) {
|
|
8291
|
+
const found = await response2.json();
|
|
8292
|
+
if (typeof found.id === "string" && Number.isSafeInteger(found.creditsQuoted) && found.creditsQuoted >= 0) {
|
|
8293
|
+
await appendAdmissionRow(cwd, {
|
|
8294
|
+
t: "q",
|
|
8295
|
+
id: found.id,
|
|
8296
|
+
kind: row.kind,
|
|
8297
|
+
prompt: row.prompt,
|
|
8298
|
+
credits: found.creditsQuoted,
|
|
8299
|
+
reservationId: requestId2,
|
|
8300
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8301
|
+
out: row.out
|
|
8302
|
+
});
|
|
8303
|
+
if (row.fingerprint === fingerprint) {
|
|
8304
|
+
input.log.dim(`Recovered generation ${found.id}; no replacement request was sent.`);
|
|
8305
|
+
return found;
|
|
8306
|
+
}
|
|
8307
|
+
continue;
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
if (row.fingerprint === fingerprint) throw new Error(`The earlier request ${requestId2} has an unknown outcome and remains reserved. Reconcile it before repeating this generation; no replacement was sent.`);
|
|
8312
|
+
}
|
|
8313
|
+
let credits = input.quotedCredits;
|
|
8314
|
+
if (generic) {
|
|
8315
|
+
const quote2 = await apiFetch(`${input.apiUrl}/api/generations/quote`, { method: "POST", headers, body: JSON.stringify(input.body) });
|
|
8316
|
+
if (!quote2.ok) {
|
|
8317
|
+
const error = await quote2.json().catch(() => ({}));
|
|
8318
|
+
throw new Error(error.message ?? `Cannot quote this generation (HTTP ${quote2.status}). No generation was sent; the API must support option-aware quotes.`);
|
|
8319
|
+
}
|
|
8320
|
+
credits = (await quote2.json()).creditsQuoted;
|
|
8321
|
+
}
|
|
8322
|
+
if (!Number.isSafeInteger(credits) || credits < 0) throw new Error("The API returned no valid generation quote; no generation was sent.");
|
|
8323
|
+
const budget = await assetBudgetGate({
|
|
8324
|
+
apiUrl: input.apiUrl,
|
|
8325
|
+
token: input.token,
|
|
8326
|
+
kind: input.kind,
|
|
8327
|
+
quotedCredits: credits,
|
|
8328
|
+
cwd,
|
|
8329
|
+
log: input.log
|
|
8330
|
+
});
|
|
8331
|
+
if (!budget.allow) throw new Error(budget.reason);
|
|
8332
|
+
const requestId = randomUUID();
|
|
8333
|
+
await appendAdmissionRow(cwd, {
|
|
8334
|
+
t: "reserve",
|
|
8335
|
+
id: requestId,
|
|
8336
|
+
credits,
|
|
8337
|
+
kind: input.kind,
|
|
8338
|
+
prompt: input.prompt.slice(0, 120),
|
|
8339
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8340
|
+
apiUrl: input.apiUrl,
|
|
8341
|
+
fingerprint,
|
|
8342
|
+
generic,
|
|
8343
|
+
out: input.outDir
|
|
8344
|
+
});
|
|
8345
|
+
const response = await apiFetch(`${input.apiUrl}${createPath}`, {
|
|
8346
|
+
method: "POST",
|
|
8347
|
+
headers,
|
|
8348
|
+
body: JSON.stringify(generic ? { ...input.body, maxCredits: credits, requestId } : input.body)
|
|
8349
|
+
});
|
|
8350
|
+
if (!response.ok) {
|
|
8351
|
+
const definiteRefusal = response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 499;
|
|
8352
|
+
if (definiteRefusal) await appendAdmissionRow(cwd, { t: "release", id: requestId });
|
|
8353
|
+
const body = await response.json().catch(() => ({}));
|
|
8354
|
+
throw new Error(body.message ?? `Generation request failed (HTTP ${response.status}); ${!definiteRefusal ? "its allowance remains reserved until reconciliation" : "no generation was accepted"}.`);
|
|
8355
|
+
}
|
|
8356
|
+
const created = await response.json();
|
|
8357
|
+
if (typeof created.id !== "string" || !created.id) throw new Error(`Generation response missing its id; request ${requestId} remains reserved.`);
|
|
8358
|
+
const authoritative = generic ? created.creditsQuoted : created.creditsQuoted ?? credits;
|
|
8359
|
+
if (!Number.isSafeInteger(authoritative) || authoritative < 0) throw new Error(`Generation ${created.id} returned an invalid quote; request remains reserved.`);
|
|
8360
|
+
await appendAdmissionRow(cwd, {
|
|
8361
|
+
t: "q",
|
|
8362
|
+
id: created.id,
|
|
8363
|
+
kind: input.kind,
|
|
8364
|
+
prompt: input.prompt.slice(0, 120),
|
|
8365
|
+
...input.prompt.length > 120 ? { tail: input.prompt.slice(-120) } : {},
|
|
8366
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8367
|
+
credits: authoritative,
|
|
8368
|
+
reservationId: requestId,
|
|
8369
|
+
...input.outDir ? { out: input.outDir } : {}
|
|
8370
|
+
});
|
|
8371
|
+
return { ...created, creditsQuoted: authoritative };
|
|
8372
|
+
});
|
|
8373
|
+
} catch (error) {
|
|
8374
|
+
input.log.error(String(error));
|
|
8375
|
+
process.exitCode = 1;
|
|
8376
|
+
return null;
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
|
|
8222
8380
|
// src/lib/lanes.ts
|
|
8223
8381
|
var LANE_CREDITS = /* @__PURE__ */ new Set(["ok", "exhausted", "unknown"]);
|
|
8224
8382
|
async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
|
|
@@ -8356,7 +8514,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
8356
8514
|
async function inlineLocalImage(filePath, flag) {
|
|
8357
8515
|
let bytes;
|
|
8358
8516
|
try {
|
|
8359
|
-
bytes = await
|
|
8517
|
+
bytes = await fs25.readFile(filePath);
|
|
8360
8518
|
} catch {
|
|
8361
8519
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
8362
8520
|
}
|
|
@@ -8366,7 +8524,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
8366
8524
|
error: `${flag} file is ${(bytes.length / 1048576).toFixed(1)} MB \u2014 over the ~4 MB inline limit. Downscale/compress it first, or pass an asset URL instead.`
|
|
8367
8525
|
};
|
|
8368
8526
|
}
|
|
8369
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
8527
|
+
const mime = IMAGE_MIME_BY_EXT[path24.extname(filePath).toLowerCase()] ?? "image/png";
|
|
8370
8528
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
8371
8529
|
}
|
|
8372
8530
|
var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
|
|
@@ -8553,7 +8711,7 @@ async function runGenerate(kind, opts) {
|
|
|
8553
8711
|
let typedPrompt = opts.prompt?.trim();
|
|
8554
8712
|
if (!typedPrompt && kind === "model" && opts.imageUrl) {
|
|
8555
8713
|
const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
|
|
8556
|
-
typedPrompt = `from image: ${
|
|
8714
|
+
typedPrompt = `from image: ${path24.basename(ref).slice(0, 120)}`;
|
|
8557
8715
|
}
|
|
8558
8716
|
if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
|
|
8559
8717
|
log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
|
|
@@ -8602,7 +8760,7 @@ async function runGenerate(kind, opts) {
|
|
|
8602
8760
|
return;
|
|
8603
8761
|
}
|
|
8604
8762
|
try {
|
|
8605
|
-
const bytes = await
|
|
8763
|
+
const bytes = await fs25.readFile(opts.inpaintUrl);
|
|
8606
8764
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
8607
8765
|
} catch {
|
|
8608
8766
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -8706,58 +8864,17 @@ async function runGenerate(kind, opts) {
|
|
|
8706
8864
|
return;
|
|
8707
8865
|
}
|
|
8708
8866
|
}
|
|
8709
|
-
{
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
let id;
|
|
8718
|
-
let creditsQuoted;
|
|
8719
|
-
try {
|
|
8720
|
-
const res = await apiFetch(`${apiUrl}/api/generations`, {
|
|
8721
|
-
method: "POST",
|
|
8722
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
8723
|
-
body: JSON.stringify({ kind, prompt, options })
|
|
8724
|
-
});
|
|
8725
|
-
if (res.status === 401) {
|
|
8726
|
-
log.error("Unauthorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
8727
|
-
process.exitCode = 1;
|
|
8728
|
-
return;
|
|
8729
|
-
}
|
|
8730
|
-
if (printedStructuredError(res)) {
|
|
8731
|
-
process.exitCode = 1;
|
|
8732
|
-
return;
|
|
8733
|
-
}
|
|
8734
|
-
if (!res.ok) {
|
|
8735
|
-
let message;
|
|
8736
|
-
if (res.status >= 400 && res.status < 500) {
|
|
8737
|
-
try {
|
|
8738
|
-
const body = await res.json();
|
|
8739
|
-
if (typeof body.message === "string" && body.message.trim()) message = body.message;
|
|
8740
|
-
} catch {
|
|
8741
|
-
}
|
|
8742
|
-
}
|
|
8743
|
-
log.error(message ?? `Generation request failed (HTTP ${res.status}).`);
|
|
8744
|
-
process.exitCode = 1;
|
|
8745
|
-
return;
|
|
8746
|
-
}
|
|
8747
|
-
const created = await res.json();
|
|
8748
|
-
id = created.id;
|
|
8749
|
-
if (typeof created.creditsQuoted === "number" && Number.isFinite(created.creditsQuoted)) {
|
|
8750
|
-
creditsQuoted = created.creditsQuoted;
|
|
8751
|
-
}
|
|
8752
|
-
} catch (err) {
|
|
8753
|
-
log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
|
|
8754
|
-
process.exitCode = 1;
|
|
8755
|
-
return;
|
|
8756
|
-
}
|
|
8757
|
-
await recordQueued(id, kind, prompt, void 0, {
|
|
8758
|
-
credits: creditsQuoted ?? opts.quotedCredits,
|
|
8759
|
-
...opts.outDir ? { outDir: opts.outDir } : {}
|
|
8867
|
+
const created = await enqueueWithAllowance({
|
|
8868
|
+
apiUrl,
|
|
8869
|
+
token,
|
|
8870
|
+
kind,
|
|
8871
|
+
prompt,
|
|
8872
|
+
body: { kind, prompt, options },
|
|
8873
|
+
log,
|
|
8874
|
+
outDir: opts.outDir
|
|
8760
8875
|
});
|
|
8876
|
+
if (!created) return;
|
|
8877
|
+
const id = created.id;
|
|
8761
8878
|
if (opts.noWait) {
|
|
8762
8879
|
if (opts.json) {
|
|
8763
8880
|
writeJson({
|
|
@@ -8812,7 +8929,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
8812
8929
|
return;
|
|
8813
8930
|
}
|
|
8814
8931
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
8815
|
-
await
|
|
8932
|
+
await fs25.mkdir(outDir, { recursive: true });
|
|
8816
8933
|
const solved = [];
|
|
8817
8934
|
for (let i = 0; i < files.length; i++) {
|
|
8818
8935
|
const f = files[i];
|
|
@@ -8844,8 +8961,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
8844
8961
|
});
|
|
8845
8962
|
continue;
|
|
8846
8963
|
}
|
|
8847
|
-
const outPath =
|
|
8848
|
-
await
|
|
8964
|
+
const outPath = path24.join(outDir, `glass-${i + 1}.png`);
|
|
8965
|
+
await fs25.writeFile(outPath, PNG4.sync.write(r.png));
|
|
8849
8966
|
solved.push({
|
|
8850
8967
|
path: outPath,
|
|
8851
8968
|
url: f.url,
|
|
@@ -9464,8 +9581,8 @@ function writeJson(value) {
|
|
|
9464
9581
|
}
|
|
9465
9582
|
|
|
9466
9583
|
// src/commands/model-sub.ts
|
|
9467
|
-
import
|
|
9468
|
-
import
|
|
9584
|
+
import fs26 from "fs/promises";
|
|
9585
|
+
import path25 from "path";
|
|
9469
9586
|
var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
|
|
9470
9587
|
function apiErrorMessage(data, fallback) {
|
|
9471
9588
|
if (typeof data !== "object" || data === null) return fallback;
|
|
@@ -9480,29 +9597,29 @@ async function importModelFile(args) {
|
|
|
9480
9597
|
const { log } = args;
|
|
9481
9598
|
const filePath = args.filePath.trim();
|
|
9482
9599
|
if (!/\.glb$/i.test(filePath)) {
|
|
9483
|
-
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${
|
|
9600
|
+
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path25.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
|
|
9484
9601
|
return null;
|
|
9485
9602
|
}
|
|
9486
9603
|
let bytes;
|
|
9487
9604
|
try {
|
|
9488
|
-
bytes = await
|
|
9605
|
+
bytes = await fs26.readFile(filePath);
|
|
9489
9606
|
} catch {
|
|
9490
9607
|
log.error(`Couldn't read ${filePath}.`);
|
|
9491
9608
|
return null;
|
|
9492
9609
|
}
|
|
9493
9610
|
if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
|
|
9494
|
-
log.error(`${
|
|
9611
|
+
log.error(`${path25.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
|
|
9495
9612
|
return null;
|
|
9496
9613
|
}
|
|
9497
9614
|
if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
|
|
9498
|
-
log.error(`${
|
|
9615
|
+
log.error(`${path25.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
|
|
9499
9616
|
return null;
|
|
9500
9617
|
}
|
|
9501
9618
|
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
|
|
9502
9619
|
const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
|
|
9503
9620
|
method: "POST",
|
|
9504
9621
|
headers,
|
|
9505
|
-
body: JSON.stringify({ filename:
|
|
9622
|
+
body: JSON.stringify({ filename: path25.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
|
|
9506
9623
|
});
|
|
9507
9624
|
if (printedStructuredError(minted)) return null;
|
|
9508
9625
|
if (!minted.ok) {
|
|
@@ -9511,7 +9628,7 @@ async function importModelFile(args) {
|
|
|
9511
9628
|
return null;
|
|
9512
9629
|
}
|
|
9513
9630
|
const { id, uploadUrl, url } = await minted.json();
|
|
9514
|
-
log.dim(` uploading ${
|
|
9631
|
+
log.dim(` uploading ${path25.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
9515
9632
|
const put = await fetch(uploadUrl, {
|
|
9516
9633
|
method: "PUT",
|
|
9517
9634
|
headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
|
|
@@ -9659,7 +9776,7 @@ async function runModelAnimate(opts) {
|
|
|
9659
9776
|
}
|
|
9660
9777
|
|
|
9661
9778
|
// src/commands/wait.ts
|
|
9662
|
-
import
|
|
9779
|
+
import path26 from "path";
|
|
9663
9780
|
var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
9664
9781
|
async function runWait(opts) {
|
|
9665
9782
|
if (opts.all) return runWaitAll(opts);
|
|
@@ -9798,7 +9915,7 @@ async function runWaitAll(opts) {
|
|
|
9798
9915
|
if (v?.status !== "completed" || !v.files?.length) continue;
|
|
9799
9916
|
const local = localDeliveryFor("tools", { ...opts, outDir: opts.outDir ?? e.outDir }, e.prompt || e.kind);
|
|
9800
9917
|
if (!local) continue;
|
|
9801
|
-
const outDir =
|
|
9918
|
+
const outDir = path26.join(path26.relative(process.cwd(), cwd) || ".", local.outDir);
|
|
9802
9919
|
const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
|
|
9803
9920
|
const missing = await undeliveredFiles(v.files, target);
|
|
9804
9921
|
if (missing.length === 0) continue;
|
|
@@ -9906,11 +10023,11 @@ async function toRow(e, v, cwd) {
|
|
|
9906
10023
|
}
|
|
9907
10024
|
|
|
9908
10025
|
// src/commands/controller.ts
|
|
9909
|
-
import
|
|
9910
|
-
import
|
|
10026
|
+
import fs28 from "fs/promises";
|
|
10027
|
+
import path28 from "path";
|
|
9911
10028
|
|
|
9912
10029
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
9913
|
-
import { createHash } from "crypto";
|
|
10030
|
+
import { createHash as createHash2 } from "crypto";
|
|
9914
10031
|
|
|
9915
10032
|
// ../../packages/meshy-animation-catalog/src/generated.ts
|
|
9916
10033
|
var MESHY_ANIMATION_CATALOG = [
|
|
@@ -18914,7 +19031,7 @@ function normalizedPackDefinition(definition) {
|
|
|
18914
19031
|
return { key, version: definition.version, actionIds, bindings };
|
|
18915
19032
|
}
|
|
18916
19033
|
function packFingerprint(definition) {
|
|
18917
|
-
return
|
|
19034
|
+
return createHash2("sha256").update(JSON.stringify(definition)).digest("hex");
|
|
18918
19035
|
}
|
|
18919
19036
|
function createMeshyControllerPackSnapshot(definition) {
|
|
18920
19037
|
const normalized = normalizedPackDefinition(definition);
|
|
@@ -19037,9 +19154,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
19037
19154
|
}
|
|
19038
19155
|
|
|
19039
19156
|
// src/lib/anims.ts
|
|
19040
|
-
import
|
|
19041
|
-
import
|
|
19042
|
-
var ANIMS_DEST =
|
|
19157
|
+
import fs27 from "fs/promises";
|
|
19158
|
+
import path27 from "path";
|
|
19159
|
+
var ANIMS_DEST = path27.join("public", "assets", "anims");
|
|
19043
19160
|
var HIDDEN_TAG = "reference";
|
|
19044
19161
|
async function runAnims(opts) {
|
|
19045
19162
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -19055,7 +19172,7 @@ async function runAnims(opts) {
|
|
|
19055
19172
|
printCatalog(log, manifest, selectors);
|
|
19056
19173
|
return;
|
|
19057
19174
|
}
|
|
19058
|
-
const controllerMarker =
|
|
19175
|
+
const controllerMarker = path27.join(root, "src", "controllers", "character");
|
|
19059
19176
|
if (!await exists3(controllerMarker)) {
|
|
19060
19177
|
log.error(
|
|
19061
19178
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -19064,11 +19181,11 @@ async function runAnims(opts) {
|
|
|
19064
19181
|
process.exitCode = 1;
|
|
19065
19182
|
return;
|
|
19066
19183
|
}
|
|
19067
|
-
const destDir =
|
|
19068
|
-
const gameManifestPath =
|
|
19184
|
+
const destDir = path27.join(root, ANIMS_DEST);
|
|
19185
|
+
const gameManifestPath = path27.join(destDir, "manifest.json");
|
|
19069
19186
|
if (opts.reset) {
|
|
19070
|
-
await
|
|
19071
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
19187
|
+
await fs27.rm(destDir, { recursive: true, force: true });
|
|
19188
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path27.sep)} (--reset)`);
|
|
19072
19189
|
}
|
|
19073
19190
|
if (selectors.length === 0) {
|
|
19074
19191
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -19106,35 +19223,35 @@ async function runAnims(opts) {
|
|
|
19106
19223
|
}
|
|
19107
19224
|
}
|
|
19108
19225
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
19109
|
-
const cacheDir =
|
|
19226
|
+
const cacheDir = path27.join(
|
|
19110
19227
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
19111
19228
|
`${manifest.library}-v${manifest.version}`
|
|
19112
19229
|
);
|
|
19113
|
-
await
|
|
19114
|
-
await
|
|
19230
|
+
await fs27.mkdir(cacheDir, { recursive: true });
|
|
19231
|
+
await fs27.mkdir(destDir, { recursive: true });
|
|
19115
19232
|
const base = getAnimsBase(opts.animsBase);
|
|
19116
19233
|
let installedCount = 0;
|
|
19117
19234
|
let presentCount = 0;
|
|
19118
19235
|
let addedBytes = 0;
|
|
19119
19236
|
const failures = [];
|
|
19120
19237
|
for (const entry of wanted) {
|
|
19121
|
-
const dest =
|
|
19238
|
+
const dest = path27.join(destDir, entry.file);
|
|
19122
19239
|
if (await hasSize(dest, entry.bytes)) {
|
|
19123
19240
|
presentCount++;
|
|
19124
19241
|
continue;
|
|
19125
19242
|
}
|
|
19126
19243
|
try {
|
|
19127
|
-
const cached =
|
|
19244
|
+
const cached = path27.join(cacheDir, entry.file);
|
|
19128
19245
|
if (!await hasSize(cached, entry.bytes)) {
|
|
19129
19246
|
const res = await fetch(base + entry.file);
|
|
19130
19247
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
19131
19248
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
19132
|
-
await
|
|
19249
|
+
await fs27.writeFile(cached, buf);
|
|
19133
19250
|
}
|
|
19134
|
-
await
|
|
19251
|
+
await fs27.copyFile(cached, dest);
|
|
19135
19252
|
installedCount++;
|
|
19136
19253
|
addedBytes += entry.bytes;
|
|
19137
|
-
log.dim(` ${
|
|
19254
|
+
log.dim(` ${path27.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
19138
19255
|
} catch (err) {
|
|
19139
19256
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
19140
19257
|
}
|
|
@@ -19150,13 +19267,13 @@ async function runAnims(opts) {
|
|
|
19150
19267
|
version: manifest.version,
|
|
19151
19268
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
19152
19269
|
};
|
|
19153
|
-
await
|
|
19270
|
+
await fs27.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
19154
19271
|
log.plain("");
|
|
19155
19272
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
19156
19273
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
19157
19274
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
19158
19275
|
log.success(
|
|
19159
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
19276
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path27.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
19160
19277
|
);
|
|
19161
19278
|
for (const [selector, entries] of resolved) {
|
|
19162
19279
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -19188,8 +19305,8 @@ async function loadManifest(baseOverride) {
|
|
|
19188
19305
|
}
|
|
19189
19306
|
} catch {
|
|
19190
19307
|
}
|
|
19191
|
-
const snapshotPath =
|
|
19192
|
-
const manifest = JSON.parse(await
|
|
19308
|
+
const snapshotPath = path27.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
19309
|
+
const manifest = JSON.parse(await fs27.readFile(snapshotPath, "utf8"));
|
|
19193
19310
|
return { manifest, source: "snapshot" };
|
|
19194
19311
|
}
|
|
19195
19312
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -19307,21 +19424,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
19307
19424
|
}
|
|
19308
19425
|
async function readGameManifest(file) {
|
|
19309
19426
|
try {
|
|
19310
|
-
return JSON.parse(await
|
|
19427
|
+
return JSON.parse(await fs27.readFile(file, "utf8"));
|
|
19311
19428
|
} catch {
|
|
19312
19429
|
return null;
|
|
19313
19430
|
}
|
|
19314
19431
|
}
|
|
19315
19432
|
async function hasSize(file, bytes) {
|
|
19316
19433
|
try {
|
|
19317
|
-
return (await
|
|
19434
|
+
return (await fs27.stat(file)).size === bytes;
|
|
19318
19435
|
} catch {
|
|
19319
19436
|
return false;
|
|
19320
19437
|
}
|
|
19321
19438
|
}
|
|
19322
19439
|
async function exists3(p) {
|
|
19323
19440
|
try {
|
|
19324
|
-
await
|
|
19441
|
+
await fs27.access(p);
|
|
19325
19442
|
return true;
|
|
19326
19443
|
} catch {
|
|
19327
19444
|
return false;
|
|
@@ -19522,8 +19639,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
19522
19639
|
]
|
|
19523
19640
|
}
|
|
19524
19641
|
};
|
|
19525
|
-
var CODE_DEST =
|
|
19526
|
-
var ASSETS_DEST =
|
|
19642
|
+
var CODE_DEST = path28.join("src", "controllers");
|
|
19643
|
+
var ASSETS_DEST = path28.join("public", "assets");
|
|
19527
19644
|
async function runController(opts) {
|
|
19528
19645
|
const log = createLogger({ quiet: opts.quiet });
|
|
19529
19646
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -19540,31 +19657,31 @@ async function runController(opts) {
|
|
|
19540
19657
|
process.exitCode = 1;
|
|
19541
19658
|
return;
|
|
19542
19659
|
}
|
|
19543
|
-
const srcDir =
|
|
19660
|
+
const srcDir = path28.join(getTemplatesDir(), "controllers");
|
|
19544
19661
|
const root = opts.cwd ?? process.cwd();
|
|
19545
19662
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
19546
19663
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
19547
19664
|
log.plain("");
|
|
19548
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
19665
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path28.sep)}`);
|
|
19549
19666
|
const plan = [
|
|
19550
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
19667
|
+
...set.code.map((rel) => ({ from: rel, rel: path28.join(CODE_DEST, rel) })),
|
|
19551
19668
|
...set.assets.map((rel) => ({
|
|
19552
19669
|
from: rel,
|
|
19553
|
-
rel:
|
|
19670
|
+
rel: path28.join(ASSETS_DEST, path28.basename(rel))
|
|
19554
19671
|
}))
|
|
19555
19672
|
];
|
|
19556
19673
|
let copied = 0;
|
|
19557
19674
|
let skipped = 0;
|
|
19558
19675
|
try {
|
|
19559
19676
|
for (const file of plan) {
|
|
19560
|
-
const dest =
|
|
19677
|
+
const dest = path28.join(root, file.rel);
|
|
19561
19678
|
if (!opts.force && await exists4(dest)) {
|
|
19562
19679
|
skipped++;
|
|
19563
19680
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
19564
19681
|
continue;
|
|
19565
19682
|
}
|
|
19566
|
-
await
|
|
19567
|
-
await
|
|
19683
|
+
await fs28.mkdir(path28.dirname(dest), { recursive: true });
|
|
19684
|
+
await fs28.copyFile(path28.join(srcDir, file.from), dest);
|
|
19568
19685
|
copied++;
|
|
19569
19686
|
log.dim(` ${file.rel}`);
|
|
19570
19687
|
}
|
|
@@ -19617,7 +19734,7 @@ async function runController(opts) {
|
|
|
19617
19734
|
for (const line of set.sketch) {
|
|
19618
19735
|
log.dim(` ${line}`);
|
|
19619
19736
|
}
|
|
19620
|
-
if (kind === "character" && !await exists4(
|
|
19737
|
+
if (kind === "character" && !await exists4(path28.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
19621
19738
|
log.plain("");
|
|
19622
19739
|
log.plain(
|
|
19623
19740
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -19649,9 +19766,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
19649
19766
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
19650
19767
|
}
|
|
19651
19768
|
assertCompleteMeshyControllerPack(manifest);
|
|
19652
|
-
const destination =
|
|
19653
|
-
await
|
|
19654
|
-
await
|
|
19769
|
+
const destination = path28.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
19770
|
+
await fs28.mkdir(path28.dirname(destination), { recursive: true });
|
|
19771
|
+
await fs28.writeFile(
|
|
19655
19772
|
destination,
|
|
19656
19773
|
`${JSON.stringify(manifest, null, 2)}
|
|
19657
19774
|
`
|
|
@@ -19796,14 +19913,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
19796
19913
|
}
|
|
19797
19914
|
async function installFallbackAvatar(args) {
|
|
19798
19915
|
const { root, srcDir, log } = args;
|
|
19799
|
-
const dest =
|
|
19800
|
-
await
|
|
19801
|
-
await
|
|
19916
|
+
const dest = path28.join(root, ASSETS_DEST, "avatar.vrm");
|
|
19917
|
+
await fs28.mkdir(path28.dirname(dest), { recursive: true });
|
|
19918
|
+
await fs28.copyFile(path28.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
19802
19919
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
19803
19920
|
}
|
|
19804
19921
|
async function exists4(p) {
|
|
19805
19922
|
try {
|
|
19806
|
-
await
|
|
19923
|
+
await fs28.access(p);
|
|
19807
19924
|
return true;
|
|
19808
19925
|
} catch {
|
|
19809
19926
|
return false;
|
|
@@ -19811,8 +19928,8 @@ async function exists4(p) {
|
|
|
19811
19928
|
}
|
|
19812
19929
|
|
|
19813
19930
|
// src/commands/character.ts
|
|
19814
|
-
import
|
|
19815
|
-
import
|
|
19931
|
+
import fs29 from "fs/promises";
|
|
19932
|
+
import path29 from "path";
|
|
19816
19933
|
function exactAnimation(selector) {
|
|
19817
19934
|
const trimmed = selector.trim();
|
|
19818
19935
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -19892,7 +20009,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
19892
20009
|
}
|
|
19893
20010
|
process.exitCode = 1;
|
|
19894
20011
|
}
|
|
19895
|
-
var INSTALLED_MANIFEST =
|
|
20012
|
+
var INSTALLED_MANIFEST = path29.join("public", "assets", "meshy-character.json");
|
|
19896
20013
|
async function resolveAdoptTarget(selector) {
|
|
19897
20014
|
const trimmed = selector?.trim();
|
|
19898
20015
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -19901,7 +20018,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
19901
20018
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
19902
20019
|
let raw;
|
|
19903
20020
|
try {
|
|
19904
|
-
raw = await
|
|
20021
|
+
raw = await fs29.readFile(file, "utf8");
|
|
19905
20022
|
} catch {
|
|
19906
20023
|
return {
|
|
19907
20024
|
ok: false,
|
|
@@ -20024,7 +20141,7 @@ async function runCharacterImport(opts) {
|
|
|
20024
20141
|
opts,
|
|
20025
20142
|
ctx,
|
|
20026
20143
|
kind: "character",
|
|
20027
|
-
prompt: `Import ${
|
|
20144
|
+
prompt: `Import ${path29.basename(filePath)} as a rigged character`,
|
|
20028
20145
|
createPath: "/api/characters/import",
|
|
20029
20146
|
body,
|
|
20030
20147
|
quote: price,
|
|
@@ -20075,23 +20192,6 @@ function requestedBuild(opts, actionIds) {
|
|
|
20075
20192
|
...opts.height === void 0 ? {} : { heightMeters: opts.height }
|
|
20076
20193
|
};
|
|
20077
20194
|
}
|
|
20078
|
-
async function postWorkflow(url, token, body) {
|
|
20079
|
-
const response = await apiFetch(url, {
|
|
20080
|
-
method: "POST",
|
|
20081
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
20082
|
-
body: JSON.stringify(body)
|
|
20083
|
-
});
|
|
20084
|
-
if (printedStructuredError(response)) return null;
|
|
20085
|
-
if (!response.ok) {
|
|
20086
|
-
const data2 = await response.json().catch(() => ({}));
|
|
20087
|
-
throw new Error(apiErrorMessage2(data2, `Character workflow request failed (HTTP ${response.status}).`));
|
|
20088
|
-
}
|
|
20089
|
-
const data = await response.json();
|
|
20090
|
-
if (typeof data.id !== "string" || data.id.length === 0) {
|
|
20091
|
-
throw new Error("Character workflow request returned no generation id.");
|
|
20092
|
-
}
|
|
20093
|
-
return { id: data.id, kind: data.kind, status: data.status, creditsQuoted: data.creditsQuoted };
|
|
20094
|
-
}
|
|
20095
20195
|
async function runWorkflow(args) {
|
|
20096
20196
|
const log = createLogger({ quiet: args.opts.quiet || args.opts.json });
|
|
20097
20197
|
if (await workspaceMode() !== "tools") {
|
|
@@ -20103,29 +20203,18 @@ async function runWorkflow(args) {
|
|
|
20103
20203
|
return;
|
|
20104
20204
|
}
|
|
20105
20205
|
}
|
|
20106
|
-
{
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
process.exitCode = 1;
|
|
20117
|
-
return;
|
|
20118
|
-
}
|
|
20119
|
-
}
|
|
20120
|
-
const created = await postWorkflow(`${args.ctx.apiUrl}${args.createPath}`, args.ctx.token, args.body);
|
|
20121
|
-
if (!created) {
|
|
20122
|
-
process.exitCode = 1;
|
|
20123
|
-
return;
|
|
20124
|
-
}
|
|
20125
|
-
await recordQueued(created.id, args.kind, args.prompt, void 0, {
|
|
20126
|
-
credits: created.creditsQuoted ?? args.quotedCredits,
|
|
20127
|
-
...args.opts.outDir ? { outDir: args.opts.outDir } : {}
|
|
20206
|
+
const created = await enqueueWithAllowance({
|
|
20207
|
+
apiUrl: args.ctx.apiUrl,
|
|
20208
|
+
token: args.ctx.token,
|
|
20209
|
+
kind: args.kind,
|
|
20210
|
+
prompt: args.prompt,
|
|
20211
|
+
body: args.body,
|
|
20212
|
+
createPath: args.createPath,
|
|
20213
|
+
quotedCredits: args.quotedCredits,
|
|
20214
|
+
outDir: args.opts.outDir,
|
|
20215
|
+
log
|
|
20128
20216
|
});
|
|
20217
|
+
if (!created) return;
|
|
20129
20218
|
if (args.opts.noWait) {
|
|
20130
20219
|
const nextCommand = `genex wait ${created.id}${args.opts.json ? " --json" : ""}`;
|
|
20131
20220
|
if (args.opts.json) {
|
|
@@ -20527,22 +20616,22 @@ async function context2(opts) {
|
|
|
20527
20616
|
const project = await readProject();
|
|
20528
20617
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
20529
20618
|
}
|
|
20530
|
-
async function readVideo(
|
|
20619
|
+
async function readVideo(path36, log) {
|
|
20531
20620
|
let bytes;
|
|
20532
20621
|
try {
|
|
20533
|
-
bytes = await readFile(
|
|
20622
|
+
bytes = await readFile(path36);
|
|
20534
20623
|
} catch {
|
|
20535
|
-
log.error(`Can't read ${
|
|
20624
|
+
log.error(`Can't read ${path36}.`);
|
|
20536
20625
|
return null;
|
|
20537
20626
|
}
|
|
20538
20627
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
20539
|
-
log.error(`${basename(
|
|
20628
|
+
log.error(`${basename(path36)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
20540
20629
|
return null;
|
|
20541
20630
|
}
|
|
20542
20631
|
return bytes;
|
|
20543
20632
|
}
|
|
20544
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
20545
|
-
const contentType = /\.mov$/i.test(
|
|
20633
|
+
async function uploadVideo(apiUrl, token, characterId, path36, bytes, log) {
|
|
20634
|
+
const contentType = /\.mov$/i.test(path36) ? "video/quicktime" : "video/mp4";
|
|
20546
20635
|
const minted = await apiFetch(
|
|
20547
20636
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
20548
20637
|
{
|
|
@@ -20557,7 +20646,7 @@ async function uploadVideo(apiUrl, token, characterId, path35, bytes, log) {
|
|
|
20557
20646
|
return null;
|
|
20558
20647
|
}
|
|
20559
20648
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
20560
|
-
log.dim(` uploading ${basename(
|
|
20649
|
+
log.dim(` uploading ${basename(path36)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
20561
20650
|
const put = await fetch(uploadUrl, {
|
|
20562
20651
|
method: "PUT",
|
|
20563
20652
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -20889,8 +20978,8 @@ function rank(items, query) {
|
|
|
20889
20978
|
}
|
|
20890
20979
|
|
|
20891
20980
|
// src/commands/motion.ts
|
|
20892
|
-
import
|
|
20893
|
-
import
|
|
20981
|
+
import fs30 from "fs/promises";
|
|
20982
|
+
import path30 from "path";
|
|
20894
20983
|
|
|
20895
20984
|
// src/lib/motion/npz.ts
|
|
20896
20985
|
import zlib from "zlib";
|
|
@@ -22141,7 +22230,7 @@ async function motionGen(opts, log) {
|
|
|
22141
22230
|
}
|
|
22142
22231
|
if (opts.constraintsPath !== void 0) {
|
|
22143
22232
|
try {
|
|
22144
|
-
const raw = await
|
|
22233
|
+
const raw = await fs30.readFile(opts.constraintsPath, "utf8");
|
|
22145
22234
|
generationOptions.constraints = JSON.parse(raw);
|
|
22146
22235
|
} catch {
|
|
22147
22236
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -22165,10 +22254,10 @@ async function motionGen(opts, log) {
|
|
|
22165
22254
|
async function expandTakes(selectors) {
|
|
22166
22255
|
const out = [];
|
|
22167
22256
|
for (const sel of selectors) {
|
|
22168
|
-
const st = await
|
|
22257
|
+
const st = await fs30.stat(sel).catch(() => null);
|
|
22169
22258
|
if (st?.isDirectory()) {
|
|
22170
|
-
const names = await
|
|
22171
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
22259
|
+
const names = await fs30.readdir(sel);
|
|
22260
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path30.join(sel, n));
|
|
22172
22261
|
} else if (st?.isFile()) {
|
|
22173
22262
|
out.push(sel);
|
|
22174
22263
|
} else {
|
|
@@ -22203,7 +22292,7 @@ async function motionVerify(opts, log) {
|
|
|
22203
22292
|
let gates = DEFAULT_GATES;
|
|
22204
22293
|
if (opts.gatesPath) {
|
|
22205
22294
|
try {
|
|
22206
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
22295
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs30.readFile(opts.gatesPath, "utf8")));
|
|
22207
22296
|
} catch {
|
|
22208
22297
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
22209
22298
|
process.exitCode = 1;
|
|
@@ -22225,9 +22314,9 @@ async function motionVerify(opts, log) {
|
|
|
22225
22314
|
}
|
|
22226
22315
|
const reports = [];
|
|
22227
22316
|
for (const file of files) {
|
|
22228
|
-
const stem =
|
|
22317
|
+
const stem = path30.basename(file).replace(/\.npz$/, "");
|
|
22229
22318
|
try {
|
|
22230
|
-
reports.push(analyzeTake(stem, await
|
|
22319
|
+
reports.push(analyzeTake(stem, await fs30.readFile(file), gates));
|
|
22231
22320
|
} catch (err) {
|
|
22232
22321
|
reports.push({
|
|
22233
22322
|
take: stem,
|
|
@@ -22265,7 +22354,7 @@ async function motionCompile(opts, log) {
|
|
|
22265
22354
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
22266
22355
|
if (opts.configPath) {
|
|
22267
22356
|
try {
|
|
22268
|
-
const patch = JSON.parse(await
|
|
22357
|
+
const patch = JSON.parse(await fs30.readFile(opts.configPath, "utf8"));
|
|
22269
22358
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
22270
22359
|
} catch {
|
|
22271
22360
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -22283,16 +22372,16 @@ async function motionCompile(opts, log) {
|
|
|
22283
22372
|
}
|
|
22284
22373
|
const inputs = [];
|
|
22285
22374
|
for (const file of files) {
|
|
22286
|
-
const stem =
|
|
22375
|
+
const stem = path30.basename(file).replace(/\.npz$/, "");
|
|
22287
22376
|
try {
|
|
22288
|
-
inputs.push({ stem, take: loadTake(await
|
|
22377
|
+
inputs.push({ stem, take: loadTake(await fs30.readFile(file)) });
|
|
22289
22378
|
} catch (err) {
|
|
22290
22379
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
22291
22380
|
process.exitCode = 1;
|
|
22292
22381
|
return;
|
|
22293
22382
|
}
|
|
22294
22383
|
}
|
|
22295
|
-
const setName = opts.set ??
|
|
22384
|
+
const setName = opts.set ?? path30.basename(opts.out).replace(/\.json$/, "");
|
|
22296
22385
|
let result;
|
|
22297
22386
|
try {
|
|
22298
22387
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -22307,9 +22396,9 @@ async function motionCompile(opts, log) {
|
|
|
22307
22396
|
process.exitCode = 1;
|
|
22308
22397
|
return;
|
|
22309
22398
|
}
|
|
22310
|
-
await
|
|
22399
|
+
await fs30.mkdir(path30.dirname(path30.resolve(opts.out)), { recursive: true });
|
|
22311
22400
|
const json = JSON.stringify(result.data);
|
|
22312
|
-
await
|
|
22401
|
+
await fs30.writeFile(opts.out, json);
|
|
22313
22402
|
if (opts.json) {
|
|
22314
22403
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
22315
22404
|
return;
|
|
@@ -22327,9 +22416,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
22327
22416
|
var MOTION_PRESETS = {
|
|
22328
22417
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
22329
22418
|
};
|
|
22330
|
-
var MOTION_DEST =
|
|
22419
|
+
var MOTION_DEST = path30.join("src", "motion");
|
|
22331
22420
|
async function motionInstall(opts, log) {
|
|
22332
|
-
const srcDir =
|
|
22421
|
+
const srcDir = path30.join(getTemplatesDir(), "motion");
|
|
22333
22422
|
const root = opts.cwd ?? process.cwd();
|
|
22334
22423
|
const preset = opts.set;
|
|
22335
22424
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -22340,21 +22429,21 @@ async function motionInstall(opts, log) {
|
|
|
22340
22429
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
22341
22430
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
22342
22431
|
log.plain("");
|
|
22343
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
22432
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path30.sep)}`);
|
|
22344
22433
|
let copied = 0, skipped = 0;
|
|
22345
22434
|
try {
|
|
22346
22435
|
for (const rel of files) {
|
|
22347
|
-
const dest =
|
|
22348
|
-
const exists5 = await
|
|
22436
|
+
const dest = path30.join(root, MOTION_DEST, rel);
|
|
22437
|
+
const exists5 = await fs30.access(dest).then(() => true, () => false);
|
|
22349
22438
|
if (!opts.force && exists5) {
|
|
22350
22439
|
skipped++;
|
|
22351
|
-
log.dim(` skipped ${
|
|
22440
|
+
log.dim(` skipped ${path30.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
22352
22441
|
continue;
|
|
22353
22442
|
}
|
|
22354
|
-
await
|
|
22355
|
-
await
|
|
22443
|
+
await fs30.mkdir(path30.dirname(dest), { recursive: true });
|
|
22444
|
+
await fs30.copyFile(path30.join(srcDir, rel), dest);
|
|
22356
22445
|
copied++;
|
|
22357
|
-
log.dim(` ${
|
|
22446
|
+
log.dim(` ${path30.join(MOTION_DEST, rel)}`);
|
|
22358
22447
|
}
|
|
22359
22448
|
} catch (err) {
|
|
22360
22449
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -22395,7 +22484,7 @@ async function motionConstraints(opts, log) {
|
|
|
22395
22484
|
}
|
|
22396
22485
|
const doc = directionConstraint(dir, speed, duration);
|
|
22397
22486
|
const out = opts.out ?? "constraints.json";
|
|
22398
|
-
await
|
|
22487
|
+
await fs30.writeFile(out, JSON.stringify(doc));
|
|
22399
22488
|
if (opts.json) {
|
|
22400
22489
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
22401
22490
|
return;
|
|
@@ -22432,14 +22521,14 @@ async function runMotion(opts) {
|
|
|
22432
22521
|
}
|
|
22433
22522
|
|
|
22434
22523
|
// src/commands/blender.ts
|
|
22435
|
-
import
|
|
22436
|
-
import
|
|
22524
|
+
import fs31 from "fs/promises";
|
|
22525
|
+
import path31 from "path";
|
|
22437
22526
|
var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
|
|
22438
22527
|
var DEFAULT_OUT_DIR = "assets/blender";
|
|
22439
22528
|
async function writeB64(dir, name, b64) {
|
|
22440
|
-
await
|
|
22441
|
-
const p =
|
|
22442
|
-
await
|
|
22529
|
+
await fs31.mkdir(dir, { recursive: true });
|
|
22530
|
+
const p = path31.join(dir, name);
|
|
22531
|
+
await fs31.writeFile(p, Buffer.from(b64, "base64"));
|
|
22443
22532
|
return p;
|
|
22444
22533
|
}
|
|
22445
22534
|
function reportScene(log, s) {
|
|
@@ -22523,7 +22612,7 @@ async function runBlender(opts) {
|
|
|
22523
22612
|
log.plain(rest.join("\n"));
|
|
22524
22613
|
return 1;
|
|
22525
22614
|
}
|
|
22526
|
-
const outDir =
|
|
22615
|
+
const outDir = path31.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
|
|
22527
22616
|
const mode = opts.mode;
|
|
22528
22617
|
if (mode !== void 0 && !isRenderMode(mode)) {
|
|
22529
22618
|
log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
|
|
@@ -22563,14 +22652,14 @@ async function runBlender(opts) {
|
|
|
22563
22652
|
return 0;
|
|
22564
22653
|
}
|
|
22565
22654
|
case "export": {
|
|
22566
|
-
const target = opts.out ??
|
|
22655
|
+
const target = opts.out ?? path31.join(outDir, "scene.glb");
|
|
22567
22656
|
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
22568
22657
|
if (!r.glbBase64) {
|
|
22569
22658
|
log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
|
|
22570
22659
|
return 1;
|
|
22571
22660
|
}
|
|
22572
|
-
await
|
|
22573
|
-
await
|
|
22661
|
+
await fs31.mkdir(path31.dirname(target), { recursive: true });
|
|
22662
|
+
await fs31.writeFile(target, Buffer.from(r.glbBase64, "base64"));
|
|
22574
22663
|
log.success(`Exported ${r.bytes ?? 0} bytes`);
|
|
22575
22664
|
log.plain(` ${c.cyan(target)}`);
|
|
22576
22665
|
return 0;
|
|
@@ -22603,12 +22692,12 @@ async function runBlender(opts) {
|
|
|
22603
22692
|
return 1;
|
|
22604
22693
|
}
|
|
22605
22694
|
try {
|
|
22606
|
-
script = await
|
|
22695
|
+
script = await fs31.readFile(opts.input, "utf8");
|
|
22607
22696
|
} catch {
|
|
22608
22697
|
log.error(`Can't read ${opts.input}`);
|
|
22609
22698
|
return 1;
|
|
22610
22699
|
}
|
|
22611
|
-
label =
|
|
22700
|
+
label = path31.basename(opts.input);
|
|
22612
22701
|
}
|
|
22613
22702
|
const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
|
|
22614
22703
|
if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
|
|
@@ -22727,9 +22816,9 @@ print(f"castle: {n} objects")
|
|
|
22727
22816
|
`;
|
|
22728
22817
|
|
|
22729
22818
|
// src/commands/asset-new.ts
|
|
22730
|
-
import
|
|
22819
|
+
import fs32 from "fs";
|
|
22731
22820
|
import fsp from "fs/promises";
|
|
22732
|
-
import
|
|
22821
|
+
import path32 from "path";
|
|
22733
22822
|
import { pathToFileURL } from "url";
|
|
22734
22823
|
var EXTRA_FILES = [
|
|
22735
22824
|
"genex-asset.example.json",
|
|
@@ -22823,7 +22912,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
22823
22912
|
}
|
|
22824
22913
|
async function runAssetNew(options) {
|
|
22825
22914
|
const log = createLogger();
|
|
22826
|
-
const cwd = options.dir ?
|
|
22915
|
+
const cwd = options.dir ? path32.resolve(options.dir) : process.cwd();
|
|
22827
22916
|
const slug = options.assetSlug;
|
|
22828
22917
|
if (!slug) {
|
|
22829
22918
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -22833,14 +22922,14 @@ async function runAssetNew(options) {
|
|
|
22833
22922
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
22834
22923
|
return 1;
|
|
22835
22924
|
}
|
|
22836
|
-
const templateDir =
|
|
22837
|
-
if (!
|
|
22925
|
+
const templateDir = path32.join(getTemplatesDir(), "asset-viewer");
|
|
22926
|
+
if (!fs32.existsSync(templateDir)) {
|
|
22838
22927
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
22839
22928
|
return 1;
|
|
22840
22929
|
}
|
|
22841
|
-
const manifestTools = await import(pathToFileURL(
|
|
22930
|
+
const manifestTools = await import(pathToFileURL(path32.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
22842
22931
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
22843
|
-
const lockPath =
|
|
22932
|
+
const lockPath = path32.join(templateDir, "shared-files.sha256.json");
|
|
22844
22933
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
22845
22934
|
const actual = hashSharedFiles(templateDir);
|
|
22846
22935
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -22853,8 +22942,8 @@ async function runAssetNew(options) {
|
|
|
22853
22942
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
22854
22943
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
22855
22944
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
22856
|
-
const outDir =
|
|
22857
|
-
if (
|
|
22945
|
+
const outDir = path32.resolve(cwd, options.out ?? slug);
|
|
22946
|
+
if (fs32.existsSync(outDir) && fs32.readdirSync(outDir).length > 0 && !options.force) {
|
|
22858
22947
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
22859
22948
|
return 1;
|
|
22860
22949
|
}
|
|
@@ -22882,24 +22971,24 @@ async function runAssetNew(options) {
|
|
|
22882
22971
|
};
|
|
22883
22972
|
await fsp.mkdir(outDir, { recursive: true });
|
|
22884
22973
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
22885
|
-
const to =
|
|
22886
|
-
await fsp.mkdir(
|
|
22887
|
-
await fsp.copyFile(
|
|
22974
|
+
const to = path32.join(outDir, rel);
|
|
22975
|
+
await fsp.mkdir(path32.dirname(to), { recursive: true });
|
|
22976
|
+
await fsp.copyFile(path32.join(templateDir, rel), to);
|
|
22888
22977
|
}
|
|
22889
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
22978
|
+
const pkg = fillTemplate(await fsp.readFile(path32.join(templateDir, "package.json"), "utf8"), {
|
|
22890
22979
|
slug,
|
|
22891
22980
|
name,
|
|
22892
22981
|
version
|
|
22893
22982
|
});
|
|
22894
|
-
await fsp.writeFile(
|
|
22895
|
-
await fsp.writeFile(
|
|
22896
|
-
await fsp.writeFile(
|
|
22983
|
+
await fsp.writeFile(path32.join(outDir, "package.json"), pkg, "utf8");
|
|
22984
|
+
await fsp.writeFile(path32.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
22985
|
+
await fsp.writeFile(path32.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
22897
22986
|
await fsp.writeFile(
|
|
22898
|
-
|
|
22987
|
+
path32.join(outDir, "DESIGN.md"),
|
|
22899
22988
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
22900
22989
|
"utf8"
|
|
22901
22990
|
);
|
|
22902
|
-
const placeholder = await fsp.readFile(
|
|
22991
|
+
const placeholder = await fsp.readFile(path32.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
22903
22992
|
const seeded = seedAssetSource(placeholder, {
|
|
22904
22993
|
slug,
|
|
22905
22994
|
name,
|
|
@@ -22910,8 +22999,8 @@ async function runAssetNew(options) {
|
|
|
22910
22999
|
pascalCase
|
|
22911
23000
|
});
|
|
22912
23001
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
22913
|
-
await fsp.mkdir(
|
|
22914
|
-
await fsp.writeFile(
|
|
23002
|
+
await fsp.mkdir(path32.join(outDir, "src", "asset"), { recursive: true });
|
|
23003
|
+
await fsp.writeFile(path32.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
22915
23004
|
const copied = hashSharedFiles(outDir);
|
|
22916
23005
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
22917
23006
|
if (mismatched.length) {
|
|
@@ -22919,7 +23008,7 @@ async function runAssetNew(options) {
|
|
|
22919
23008
|
return 1;
|
|
22920
23009
|
}
|
|
22921
23010
|
await fsp.writeFile(
|
|
22922
|
-
|
|
23011
|
+
path32.join(outDir, PARITY_FILENAME),
|
|
22923
23012
|
JSON.stringify(
|
|
22924
23013
|
{
|
|
22925
23014
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -22963,10 +23052,10 @@ async function runAssetNew(options) {
|
|
|
22963
23052
|
}
|
|
22964
23053
|
|
|
22965
23054
|
// src/commands/tools.ts
|
|
22966
|
-
import
|
|
23055
|
+
import path34 from "path";
|
|
22967
23056
|
|
|
22968
23057
|
// src/commands/doctor.ts
|
|
22969
|
-
import
|
|
23058
|
+
import path33 from "path";
|
|
22970
23059
|
var LANE_ORDER = [
|
|
22971
23060
|
"model",
|
|
22972
23061
|
"image",
|
|
@@ -23247,7 +23336,7 @@ async function fetchLegalStatus(apiUrl, token) {
|
|
|
23247
23336
|
}
|
|
23248
23337
|
async function firstSkillsMarker() {
|
|
23249
23338
|
for (const target of resolveAgentTargets()) {
|
|
23250
|
-
const marker = await readSkillsMarker(
|
|
23339
|
+
const marker = await readSkillsMarker(path33.join(target.baseDir, "skills"));
|
|
23251
23340
|
if (marker) return marker;
|
|
23252
23341
|
}
|
|
23253
23342
|
return null;
|
|
@@ -23298,8 +23387,8 @@ async function runTools(opts) {
|
|
|
23298
23387
|
let totalNew = 0;
|
|
23299
23388
|
let totalUpdated = 0;
|
|
23300
23389
|
for (const t of targets) {
|
|
23301
|
-
const dest =
|
|
23302
|
-
const { copied, updated } = await copyTemplates(
|
|
23390
|
+
const dest = path34.join(t.baseDir, "skills");
|
|
23391
|
+
const { copied, updated } = await copyTemplates(path34.join(templatesDir, "skills"), dest, {
|
|
23303
23392
|
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
23304
23393
|
});
|
|
23305
23394
|
await pruneRemovedSkills(dest, log);
|
|
@@ -23358,8 +23447,8 @@ async function runTools(opts) {
|
|
|
23358
23447
|
}
|
|
23359
23448
|
|
|
23360
23449
|
// src/commands/budget.ts
|
|
23361
|
-
import
|
|
23362
|
-
import
|
|
23450
|
+
import fs33 from "fs/promises";
|
|
23451
|
+
import path35 from "path";
|
|
23363
23452
|
var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
|
|
23364
23453
|
var BUDGET_APPROVAL_REQUIRED = "STOP: raising the asset allowance requires the player's explicit approval. Re-run with --user-approved only after they agreed to the number.";
|
|
23365
23454
|
function internalKind(kind) {
|
|
@@ -23367,7 +23456,7 @@ function internalKind(kind) {
|
|
|
23367
23456
|
}
|
|
23368
23457
|
async function isGenexProject(cwd) {
|
|
23369
23458
|
try {
|
|
23370
|
-
await
|
|
23459
|
+
await fs33.access(path35.join(cwd, ".genex"));
|
|
23371
23460
|
return true;
|
|
23372
23461
|
} catch {
|
|
23373
23462
|
return false;
|