@genex-ai/cli-demo 1.32.2 → 1.32.3-dev.680
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{blender-mcp-X66ZZ4TN.js → blender-mcp-Q6PSFYSE.js} +2 -2
- package/dist/{blender-serve-PVVCH3S6.js → blender-serve-BF4FZ55Z.js} +1 -1
- package/dist/{chunk-OJAXYQ5L.js → chunk-2COG4P3T.js} +1 -1
- package/dist/{chunk-5FA2WLM7.js → chunk-HYCSNWYX.js} +1 -1
- package/dist/index.js +544 -144
- package/package.json +1 -1
- package/templates/skills/genex-ai-character/SKILL.md +7 -1
- package/templates/skills/genex-ai-model/SKILL.md +12 -0
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-game-director/SKILL.md +25 -13
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-lane-card/SKILL.md +26 -5
- package/templates/skills/genex-updates/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
writeProject,
|
|
43
43
|
writeUserToken,
|
|
44
44
|
writeWorkspace
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-2COG4P3T.js";
|
|
46
46
|
import {
|
|
47
47
|
CLI_CHANNEL,
|
|
48
48
|
DEFAULT_API_URL,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
getGenexEnvPath,
|
|
60
60
|
getTemplatesDir,
|
|
61
61
|
resolveAgentTargets
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-HYCSNWYX.js";
|
|
63
63
|
|
|
64
64
|
// src/instrument.ts
|
|
65
65
|
import * as Sentry from "@sentry/node";
|
|
@@ -1940,14 +1940,16 @@ async function readLedger(cwd = process.cwd()) {
|
|
|
1940
1940
|
}
|
|
1941
1941
|
return [...entries.values()];
|
|
1942
1942
|
}
|
|
1943
|
-
async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
|
|
1943
|
+
async function recordQueued(id, kind, prompt, cwd = process.cwd(), extra = {}) {
|
|
1944
|
+
const credits = extra.credits;
|
|
1944
1945
|
await append(cwd, {
|
|
1945
1946
|
t: "q",
|
|
1946
1947
|
id,
|
|
1947
1948
|
kind,
|
|
1948
1949
|
prompt: prompt.slice(0, 120),
|
|
1949
1950
|
...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
|
|
1950
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1951
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1952
|
+
...typeof credits === "number" && Number.isFinite(credits) && credits >= 0 ? { credits } : {}
|
|
1951
1953
|
});
|
|
1952
1954
|
}
|
|
1953
1955
|
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
@@ -7879,8 +7881,8 @@ async function runRollback(opts) {
|
|
|
7879
7881
|
}
|
|
7880
7882
|
|
|
7881
7883
|
// src/commands/generate.ts
|
|
7882
|
-
import
|
|
7883
|
-
import
|
|
7884
|
+
import fs24 from "fs/promises";
|
|
7885
|
+
import path23 from "path";
|
|
7884
7886
|
import { PNG as PNG4 } from "pngjs";
|
|
7885
7887
|
|
|
7886
7888
|
// src/lib/glass.ts
|
|
@@ -8047,6 +8049,159 @@ function ceilingVerdict(input) {
|
|
|
8047
8049
|
return { allow: true };
|
|
8048
8050
|
}
|
|
8049
8051
|
|
|
8052
|
+
// src/lib/asset-budget.ts
|
|
8053
|
+
import fs23 from "fs/promises";
|
|
8054
|
+
import path22 from "path";
|
|
8055
|
+
var DEFAULT_ASSET_BUDGET_SHARE = 0.5;
|
|
8056
|
+
var DEFAULT_ASSET_BUDGET_CAP = 500;
|
|
8057
|
+
function envShare(raw, fallback) {
|
|
8058
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
8059
|
+
const n = Number(raw);
|
|
8060
|
+
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
|
|
8061
|
+
}
|
|
8062
|
+
function envCredits(raw, fallback) {
|
|
8063
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
8064
|
+
const n = Number(raw);
|
|
8065
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
|
|
8066
|
+
}
|
|
8067
|
+
function budgetConfig(env = process.env) {
|
|
8068
|
+
return {
|
|
8069
|
+
share: envShare(env.GENEX_ASSET_BUDGET_SHARE, DEFAULT_ASSET_BUDGET_SHARE),
|
|
8070
|
+
cap: envCredits(env.GENEX_ASSET_BUDGET_CAP, DEFAULT_ASSET_BUDGET_CAP)
|
|
8071
|
+
};
|
|
8072
|
+
}
|
|
8073
|
+
function budgetDisabled(config) {
|
|
8074
|
+
return config.cap <= 0;
|
|
8075
|
+
}
|
|
8076
|
+
function allowanceFor(input) {
|
|
8077
|
+
const { spendable, config } = input;
|
|
8078
|
+
if (!Number.isFinite(spendable) || spendable <= 0) return 0;
|
|
8079
|
+
return Math.max(0, Math.min(Math.floor(spendable * config.share), config.cap));
|
|
8080
|
+
}
|
|
8081
|
+
var storePath = (cwd) => path22.join(cwd, ".genex", "asset-budget.json");
|
|
8082
|
+
async function readBudgetStore(cwd = process.cwd()) {
|
|
8083
|
+
try {
|
|
8084
|
+
const raw = await fs23.readFile(storePath(cwd), "utf8");
|
|
8085
|
+
const parsed = JSON.parse(raw);
|
|
8086
|
+
if (!parsed || parsed.v !== 1 || typeof parsed.allowanceCredits !== "number" || !Number.isFinite(parsed.allowanceCredits) || parsed.setBy !== "default" && parsed.setBy !== "user") {
|
|
8087
|
+
return null;
|
|
8088
|
+
}
|
|
8089
|
+
return {
|
|
8090
|
+
v: 1,
|
|
8091
|
+
allowanceCredits: Math.max(0, Math.floor(parsed.allowanceCredits)),
|
|
8092
|
+
setBy: parsed.setBy,
|
|
8093
|
+
setAt: typeof parsed.setAt === "string" ? parsed.setAt : "",
|
|
8094
|
+
spendableAtStart: typeof parsed.spendableAtStart === "number" ? parsed.spendableAtStart : 0,
|
|
8095
|
+
balanceAtStart: typeof parsed.balanceAtStart === "number" ? parsed.balanceAtStart : 0
|
|
8096
|
+
};
|
|
8097
|
+
} catch {
|
|
8098
|
+
return null;
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
async function writeBudgetStore(cwd, store) {
|
|
8102
|
+
try {
|
|
8103
|
+
await fs23.access(path22.join(cwd, ".genex"));
|
|
8104
|
+
await fs23.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
|
|
8105
|
+
`, "utf8");
|
|
8106
|
+
return true;
|
|
8107
|
+
} catch {
|
|
8108
|
+
return false;
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
async function ensureBudgetStore(cwd, input) {
|
|
8112
|
+
const existing = await readBudgetStore(cwd);
|
|
8113
|
+
if (existing) return existing;
|
|
8114
|
+
const store = {
|
|
8115
|
+
v: 1,
|
|
8116
|
+
allowanceCredits: allowanceFor({ spendable: input.spendable, config: input.config }),
|
|
8117
|
+
setBy: "default",
|
|
8118
|
+
setAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8119
|
+
spendableAtStart: input.spendable,
|
|
8120
|
+
balanceAtStart: input.balance
|
|
8121
|
+
};
|
|
8122
|
+
await writeBudgetStore(cwd, store);
|
|
8123
|
+
return store;
|
|
8124
|
+
}
|
|
8125
|
+
function isFailureRow(row) {
|
|
8126
|
+
return row.t === "f" || row.t === "done" && row.status === "failed";
|
|
8127
|
+
}
|
|
8128
|
+
function spentCredits(rows) {
|
|
8129
|
+
const quoted = /* @__PURE__ */ new Map();
|
|
8130
|
+
for (const row of rows) {
|
|
8131
|
+
if (typeof row.id !== "string") continue;
|
|
8132
|
+
if (row.t === "q") {
|
|
8133
|
+
if (quoted.has(row.id)) continue;
|
|
8134
|
+
const credits = typeof row.credits === "number" && Number.isFinite(row.credits) && row.credits > 0 ? row.credits : 0;
|
|
8135
|
+
quoted.set(row.id, credits);
|
|
8136
|
+
} else if (isFailureRow(row) && quoted.has(row.id)) {
|
|
8137
|
+
quoted.set(row.id, 0);
|
|
8138
|
+
}
|
|
8139
|
+
}
|
|
8140
|
+
let total = 0;
|
|
8141
|
+
for (const credits of quoted.values()) total += credits;
|
|
8142
|
+
return total;
|
|
8143
|
+
}
|
|
8144
|
+
function budgetVerdict(input) {
|
|
8145
|
+
const { kind, price, spent, allowance } = input;
|
|
8146
|
+
const config = input.config ?? budgetConfig();
|
|
8147
|
+
if (budgetDisabled(config)) return { allow: true };
|
|
8148
|
+
if (spent + price <= allowance) return { allow: true };
|
|
8149
|
+
return {
|
|
8150
|
+
allow: false,
|
|
8151
|
+
reason: `This ${kind} is quoted at ${price} credits and this build has spent ${spent} of its ${allowance}-credit asset allowance. Refused so one build cannot drain the wallet. If the player wants more of this game generated, ask them \u2014 then re-run after \`npx genex budget --assets <credits> --user-approved\`. Otherwise build it in code and say why. (See the numbers any time with \`npx genex budget\`.)`
|
|
8152
|
+
};
|
|
8153
|
+
}
|
|
8154
|
+
async function fetchCreditsSnapshot(apiUrl, token) {
|
|
8155
|
+
try {
|
|
8156
|
+
const res = await apiFetch(`${apiUrl}/api/credits/me`, {
|
|
8157
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
8158
|
+
signal: AbortSignal.timeout(6e3)
|
|
8159
|
+
});
|
|
8160
|
+
if (!res.ok) return null;
|
|
8161
|
+
const body = await res.json();
|
|
8162
|
+
if (!body || typeof body.balance !== "number" || typeof body.prices !== "object" || body.prices === null) {
|
|
8163
|
+
return null;
|
|
8164
|
+
}
|
|
8165
|
+
const balance = body.balance;
|
|
8166
|
+
const reserved = typeof body.reserved === "number" ? body.reserved : 0;
|
|
8167
|
+
return {
|
|
8168
|
+
balance,
|
|
8169
|
+
reserved,
|
|
8170
|
+
spendable: typeof body.spendable === "number" ? body.spendable : Math.max(0, balance - reserved),
|
|
8171
|
+
unlimited: body.unlimited === true,
|
|
8172
|
+
...typeof body.emailVerified === "boolean" ? { emailVerified: body.emailVerified } : {},
|
|
8173
|
+
...body.refillAt !== void 0 ? { refillAt: body.refillAt } : {},
|
|
8174
|
+
...typeof body.refillTo === "number" ? { refillTo: body.refillTo } : {},
|
|
8175
|
+
prices: body.prices
|
|
8176
|
+
};
|
|
8177
|
+
} catch {
|
|
8178
|
+
return null;
|
|
8179
|
+
}
|
|
8180
|
+
}
|
|
8181
|
+
async function assetBudgetGate(input) {
|
|
8182
|
+
const config = input.config ?? budgetConfig();
|
|
8183
|
+
if (budgetDisabled(config)) return { allow: true };
|
|
8184
|
+
const cwd = input.cwd ?? process.cwd();
|
|
8185
|
+
const snapshot = await fetchCreditsSnapshot(input.apiUrl, input.token);
|
|
8186
|
+
if (!snapshot) {
|
|
8187
|
+
input.log.dim(" Asset allowance not checked (couldn't read /api/credits/me) \u2014 the API still refuses on insufficient credits.");
|
|
8188
|
+
return { allow: true };
|
|
8189
|
+
}
|
|
8190
|
+
if (snapshot.unlimited) return { allow: true };
|
|
8191
|
+
const price = typeof input.quotedCredits === "number" && Number.isFinite(input.quotedCredits) ? input.quotedCredits : snapshot.prices[input.kind];
|
|
8192
|
+
if (typeof price !== "number" || !Number.isFinite(price)) {
|
|
8193
|
+
input.log.dim(` Asset allowance not checked (no live price for ${input.kind}).`);
|
|
8194
|
+
return { allow: true };
|
|
8195
|
+
}
|
|
8196
|
+
const store = await ensureBudgetStore(cwd, {
|
|
8197
|
+
balance: snapshot.balance,
|
|
8198
|
+
spendable: snapshot.spendable,
|
|
8199
|
+
config
|
|
8200
|
+
});
|
|
8201
|
+
const spent = spentCredits(await readLedgerRows(cwd));
|
|
8202
|
+
return budgetVerdict({ kind: input.kind, price, spent, allowance: store.allowanceCredits, config });
|
|
8203
|
+
}
|
|
8204
|
+
|
|
8050
8205
|
// src/lib/lanes.ts
|
|
8051
8206
|
var LANE_CREDITS = /* @__PURE__ */ new Set(["ok", "exhausted", "unknown"]);
|
|
8052
8207
|
async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
|
|
@@ -8184,7 +8339,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
8184
8339
|
async function inlineLocalImage(filePath, flag) {
|
|
8185
8340
|
let bytes;
|
|
8186
8341
|
try {
|
|
8187
|
-
bytes = await
|
|
8342
|
+
bytes = await fs24.readFile(filePath);
|
|
8188
8343
|
} catch {
|
|
8189
8344
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
8190
8345
|
}
|
|
@@ -8194,7 +8349,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
8194
8349
|
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.`
|
|
8195
8350
|
};
|
|
8196
8351
|
}
|
|
8197
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
8352
|
+
const mime = IMAGE_MIME_BY_EXT[path23.extname(filePath).toLowerCase()] ?? "image/png";
|
|
8198
8353
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
8199
8354
|
}
|
|
8200
8355
|
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.";
|
|
@@ -8380,7 +8535,7 @@ async function runGenerate(kind, opts) {
|
|
|
8380
8535
|
let typedPrompt = opts.prompt?.trim();
|
|
8381
8536
|
if (!typedPrompt && kind === "model" && opts.imageUrl) {
|
|
8382
8537
|
const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
|
|
8383
|
-
typedPrompt = `from image: ${
|
|
8538
|
+
typedPrompt = `from image: ${path23.basename(ref).slice(0, 120)}`;
|
|
8384
8539
|
}
|
|
8385
8540
|
if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
|
|
8386
8541
|
log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
|
|
@@ -8429,7 +8584,7 @@ async function runGenerate(kind, opts) {
|
|
|
8429
8584
|
return;
|
|
8430
8585
|
}
|
|
8431
8586
|
try {
|
|
8432
|
-
const bytes = await
|
|
8587
|
+
const bytes = await fs24.readFile(opts.inpaintUrl);
|
|
8433
8588
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
8434
8589
|
} catch {
|
|
8435
8590
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -8533,7 +8688,16 @@ async function runGenerate(kind, opts) {
|
|
|
8533
8688
|
return;
|
|
8534
8689
|
}
|
|
8535
8690
|
}
|
|
8691
|
+
{
|
|
8692
|
+
const budget = await assetBudgetGate({ apiUrl, token, kind, quotedCredits: opts.quotedCredits, log });
|
|
8693
|
+
if (!budget.allow) {
|
|
8694
|
+
log.error(budget.reason);
|
|
8695
|
+
process.exitCode = 1;
|
|
8696
|
+
return;
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8536
8699
|
let id;
|
|
8700
|
+
let creditsQuoted;
|
|
8537
8701
|
try {
|
|
8538
8702
|
const res = await apiFetch(`${apiUrl}/api/generations`, {
|
|
8539
8703
|
method: "POST",
|
|
@@ -8562,13 +8726,17 @@ async function runGenerate(kind, opts) {
|
|
|
8562
8726
|
process.exitCode = 1;
|
|
8563
8727
|
return;
|
|
8564
8728
|
}
|
|
8565
|
-
|
|
8729
|
+
const created = await res.json();
|
|
8730
|
+
id = created.id;
|
|
8731
|
+
if (typeof created.creditsQuoted === "number" && Number.isFinite(created.creditsQuoted)) {
|
|
8732
|
+
creditsQuoted = created.creditsQuoted;
|
|
8733
|
+
}
|
|
8566
8734
|
} catch (err) {
|
|
8567
8735
|
log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
|
|
8568
8736
|
process.exitCode = 1;
|
|
8569
8737
|
return;
|
|
8570
8738
|
}
|
|
8571
|
-
await recordQueued(id, kind, prompt);
|
|
8739
|
+
await recordQueued(id, kind, prompt, void 0, { credits: creditsQuoted ?? opts.quotedCredits });
|
|
8572
8740
|
if (opts.noWait) {
|
|
8573
8741
|
if (opts.json) {
|
|
8574
8742
|
writeJson({
|
|
@@ -8623,7 +8791,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
8623
8791
|
return;
|
|
8624
8792
|
}
|
|
8625
8793
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
8626
|
-
await
|
|
8794
|
+
await fs24.mkdir(outDir, { recursive: true });
|
|
8627
8795
|
const solved = [];
|
|
8628
8796
|
for (let i = 0; i < files.length; i++) {
|
|
8629
8797
|
const f = files[i];
|
|
@@ -8655,8 +8823,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
8655
8823
|
});
|
|
8656
8824
|
continue;
|
|
8657
8825
|
}
|
|
8658
|
-
const outPath =
|
|
8659
|
-
await
|
|
8826
|
+
const outPath = path23.join(outDir, `glass-${i + 1}.png`);
|
|
8827
|
+
await fs24.writeFile(outPath, PNG4.sync.write(r.png));
|
|
8660
8828
|
solved.push({
|
|
8661
8829
|
path: outPath,
|
|
8662
8830
|
url: f.url,
|
|
@@ -9174,8 +9342,8 @@ function writeJson(value) {
|
|
|
9174
9342
|
}
|
|
9175
9343
|
|
|
9176
9344
|
// src/commands/model-sub.ts
|
|
9177
|
-
import
|
|
9178
|
-
import
|
|
9345
|
+
import fs25 from "fs/promises";
|
|
9346
|
+
import path24 from "path";
|
|
9179
9347
|
var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
|
|
9180
9348
|
function apiErrorMessage(data, fallback) {
|
|
9181
9349
|
if (typeof data !== "object" || data === null) return fallback;
|
|
@@ -9190,29 +9358,29 @@ async function importModelFile(args) {
|
|
|
9190
9358
|
const { log } = args;
|
|
9191
9359
|
const filePath = args.filePath.trim();
|
|
9192
9360
|
if (!/\.glb$/i.test(filePath)) {
|
|
9193
|
-
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${
|
|
9361
|
+
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path24.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
|
|
9194
9362
|
return null;
|
|
9195
9363
|
}
|
|
9196
9364
|
let bytes;
|
|
9197
9365
|
try {
|
|
9198
|
-
bytes = await
|
|
9366
|
+
bytes = await fs25.readFile(filePath);
|
|
9199
9367
|
} catch {
|
|
9200
9368
|
log.error(`Couldn't read ${filePath}.`);
|
|
9201
9369
|
return null;
|
|
9202
9370
|
}
|
|
9203
9371
|
if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
|
|
9204
|
-
log.error(`${
|
|
9372
|
+
log.error(`${path24.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.`);
|
|
9205
9373
|
return null;
|
|
9206
9374
|
}
|
|
9207
9375
|
if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
|
|
9208
|
-
log.error(`${
|
|
9376
|
+
log.error(`${path24.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
|
|
9209
9377
|
return null;
|
|
9210
9378
|
}
|
|
9211
9379
|
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
|
|
9212
9380
|
const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
|
|
9213
9381
|
method: "POST",
|
|
9214
9382
|
headers,
|
|
9215
|
-
body: JSON.stringify({ filename:
|
|
9383
|
+
body: JSON.stringify({ filename: path24.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
|
|
9216
9384
|
});
|
|
9217
9385
|
if (printedStructuredError(minted)) return null;
|
|
9218
9386
|
if (!minted.ok) {
|
|
@@ -9221,7 +9389,7 @@ async function importModelFile(args) {
|
|
|
9221
9389
|
return null;
|
|
9222
9390
|
}
|
|
9223
9391
|
const { id, uploadUrl, url } = await minted.json();
|
|
9224
|
-
log.dim(` uploading ${
|
|
9392
|
+
log.dim(` uploading ${path24.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
9225
9393
|
const put = await fetch(uploadUrl, {
|
|
9226
9394
|
method: "PUT",
|
|
9227
9395
|
headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
|
|
@@ -9369,7 +9537,7 @@ async function runModelAnimate(opts) {
|
|
|
9369
9537
|
}
|
|
9370
9538
|
|
|
9371
9539
|
// src/commands/wait.ts
|
|
9372
|
-
import
|
|
9540
|
+
import path25 from "path";
|
|
9373
9541
|
var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
9374
9542
|
async function runWait(opts) {
|
|
9375
9543
|
if (opts.all) return runWaitAll(opts);
|
|
@@ -9500,7 +9668,7 @@ async function runWaitAll(opts) {
|
|
|
9500
9668
|
if (v?.status !== "completed" || !v.files?.length) continue;
|
|
9501
9669
|
const local = localDeliveryFor("tools", opts, e.prompt || e.kind);
|
|
9502
9670
|
if (!local) continue;
|
|
9503
|
-
const outDir =
|
|
9671
|
+
const outDir = path25.join(path25.relative(process.cwd(), cwd) || ".", local.outDir);
|
|
9504
9672
|
const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
|
|
9505
9673
|
const missing = await undeliveredFiles(v.files, target);
|
|
9506
9674
|
if (missing.length === 0) continue;
|
|
@@ -9595,8 +9763,8 @@ async function toRow(e, v, cwd) {
|
|
|
9595
9763
|
}
|
|
9596
9764
|
|
|
9597
9765
|
// src/commands/controller.ts
|
|
9598
|
-
import
|
|
9599
|
-
import
|
|
9766
|
+
import fs27 from "fs/promises";
|
|
9767
|
+
import path27 from "path";
|
|
9600
9768
|
|
|
9601
9769
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
9602
9770
|
import { createHash } from "crypto";
|
|
@@ -18726,9 +18894,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
18726
18894
|
}
|
|
18727
18895
|
|
|
18728
18896
|
// src/lib/anims.ts
|
|
18729
|
-
import
|
|
18730
|
-
import
|
|
18731
|
-
var ANIMS_DEST =
|
|
18897
|
+
import fs26 from "fs/promises";
|
|
18898
|
+
import path26 from "path";
|
|
18899
|
+
var ANIMS_DEST = path26.join("public", "assets", "anims");
|
|
18732
18900
|
var HIDDEN_TAG = "reference";
|
|
18733
18901
|
async function runAnims(opts) {
|
|
18734
18902
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -18744,7 +18912,7 @@ async function runAnims(opts) {
|
|
|
18744
18912
|
printCatalog(log, manifest, selectors);
|
|
18745
18913
|
return;
|
|
18746
18914
|
}
|
|
18747
|
-
const controllerMarker =
|
|
18915
|
+
const controllerMarker = path26.join(root, "src", "controllers", "character");
|
|
18748
18916
|
if (!await exists3(controllerMarker)) {
|
|
18749
18917
|
log.error(
|
|
18750
18918
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -18753,11 +18921,11 @@ async function runAnims(opts) {
|
|
|
18753
18921
|
process.exitCode = 1;
|
|
18754
18922
|
return;
|
|
18755
18923
|
}
|
|
18756
|
-
const destDir =
|
|
18757
|
-
const gameManifestPath =
|
|
18924
|
+
const destDir = path26.join(root, ANIMS_DEST);
|
|
18925
|
+
const gameManifestPath = path26.join(destDir, "manifest.json");
|
|
18758
18926
|
if (opts.reset) {
|
|
18759
|
-
await
|
|
18760
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
18927
|
+
await fs26.rm(destDir, { recursive: true, force: true });
|
|
18928
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path26.sep)} (--reset)`);
|
|
18761
18929
|
}
|
|
18762
18930
|
if (selectors.length === 0) {
|
|
18763
18931
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -18795,35 +18963,35 @@ async function runAnims(opts) {
|
|
|
18795
18963
|
}
|
|
18796
18964
|
}
|
|
18797
18965
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
18798
|
-
const cacheDir =
|
|
18966
|
+
const cacheDir = path26.join(
|
|
18799
18967
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
18800
18968
|
`${manifest.library}-v${manifest.version}`
|
|
18801
18969
|
);
|
|
18802
|
-
await
|
|
18803
|
-
await
|
|
18970
|
+
await fs26.mkdir(cacheDir, { recursive: true });
|
|
18971
|
+
await fs26.mkdir(destDir, { recursive: true });
|
|
18804
18972
|
const base = getAnimsBase(opts.animsBase);
|
|
18805
18973
|
let installedCount = 0;
|
|
18806
18974
|
let presentCount = 0;
|
|
18807
18975
|
let addedBytes = 0;
|
|
18808
18976
|
const failures = [];
|
|
18809
18977
|
for (const entry of wanted) {
|
|
18810
|
-
const dest =
|
|
18978
|
+
const dest = path26.join(destDir, entry.file);
|
|
18811
18979
|
if (await hasSize(dest, entry.bytes)) {
|
|
18812
18980
|
presentCount++;
|
|
18813
18981
|
continue;
|
|
18814
18982
|
}
|
|
18815
18983
|
try {
|
|
18816
|
-
const cached =
|
|
18984
|
+
const cached = path26.join(cacheDir, entry.file);
|
|
18817
18985
|
if (!await hasSize(cached, entry.bytes)) {
|
|
18818
18986
|
const res = await fetch(base + entry.file);
|
|
18819
18987
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
18820
18988
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
18821
|
-
await
|
|
18989
|
+
await fs26.writeFile(cached, buf);
|
|
18822
18990
|
}
|
|
18823
|
-
await
|
|
18991
|
+
await fs26.copyFile(cached, dest);
|
|
18824
18992
|
installedCount++;
|
|
18825
18993
|
addedBytes += entry.bytes;
|
|
18826
|
-
log.dim(` ${
|
|
18994
|
+
log.dim(` ${path26.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
18827
18995
|
} catch (err) {
|
|
18828
18996
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
18829
18997
|
}
|
|
@@ -18839,13 +19007,13 @@ async function runAnims(opts) {
|
|
|
18839
19007
|
version: manifest.version,
|
|
18840
19008
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
18841
19009
|
};
|
|
18842
|
-
await
|
|
19010
|
+
await fs26.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
18843
19011
|
log.plain("");
|
|
18844
19012
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
18845
19013
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
18846
19014
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
18847
19015
|
log.success(
|
|
18848
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
19016
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path26.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
18849
19017
|
);
|
|
18850
19018
|
for (const [selector, entries] of resolved) {
|
|
18851
19019
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -18877,8 +19045,8 @@ async function loadManifest(baseOverride) {
|
|
|
18877
19045
|
}
|
|
18878
19046
|
} catch {
|
|
18879
19047
|
}
|
|
18880
|
-
const snapshotPath =
|
|
18881
|
-
const manifest = JSON.parse(await
|
|
19048
|
+
const snapshotPath = path26.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
19049
|
+
const manifest = JSON.parse(await fs26.readFile(snapshotPath, "utf8"));
|
|
18882
19050
|
return { manifest, source: "snapshot" };
|
|
18883
19051
|
}
|
|
18884
19052
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -18996,21 +19164,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
18996
19164
|
}
|
|
18997
19165
|
async function readGameManifest(file) {
|
|
18998
19166
|
try {
|
|
18999
|
-
return JSON.parse(await
|
|
19167
|
+
return JSON.parse(await fs26.readFile(file, "utf8"));
|
|
19000
19168
|
} catch {
|
|
19001
19169
|
return null;
|
|
19002
19170
|
}
|
|
19003
19171
|
}
|
|
19004
19172
|
async function hasSize(file, bytes) {
|
|
19005
19173
|
try {
|
|
19006
|
-
return (await
|
|
19174
|
+
return (await fs26.stat(file)).size === bytes;
|
|
19007
19175
|
} catch {
|
|
19008
19176
|
return false;
|
|
19009
19177
|
}
|
|
19010
19178
|
}
|
|
19011
19179
|
async function exists3(p) {
|
|
19012
19180
|
try {
|
|
19013
|
-
await
|
|
19181
|
+
await fs26.access(p);
|
|
19014
19182
|
return true;
|
|
19015
19183
|
} catch {
|
|
19016
19184
|
return false;
|
|
@@ -19211,8 +19379,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
19211
19379
|
]
|
|
19212
19380
|
}
|
|
19213
19381
|
};
|
|
19214
|
-
var CODE_DEST =
|
|
19215
|
-
var ASSETS_DEST =
|
|
19382
|
+
var CODE_DEST = path27.join("src", "controllers");
|
|
19383
|
+
var ASSETS_DEST = path27.join("public", "assets");
|
|
19216
19384
|
async function runController(opts) {
|
|
19217
19385
|
const log = createLogger({ quiet: opts.quiet });
|
|
19218
19386
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -19229,31 +19397,31 @@ async function runController(opts) {
|
|
|
19229
19397
|
process.exitCode = 1;
|
|
19230
19398
|
return;
|
|
19231
19399
|
}
|
|
19232
|
-
const srcDir =
|
|
19400
|
+
const srcDir = path27.join(getTemplatesDir(), "controllers");
|
|
19233
19401
|
const root = opts.cwd ?? process.cwd();
|
|
19234
19402
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
19235
19403
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
19236
19404
|
log.plain("");
|
|
19237
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
19405
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path27.sep)}`);
|
|
19238
19406
|
const plan = [
|
|
19239
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
19407
|
+
...set.code.map((rel) => ({ from: rel, rel: path27.join(CODE_DEST, rel) })),
|
|
19240
19408
|
...set.assets.map((rel) => ({
|
|
19241
19409
|
from: rel,
|
|
19242
|
-
rel:
|
|
19410
|
+
rel: path27.join(ASSETS_DEST, path27.basename(rel))
|
|
19243
19411
|
}))
|
|
19244
19412
|
];
|
|
19245
19413
|
let copied = 0;
|
|
19246
19414
|
let skipped = 0;
|
|
19247
19415
|
try {
|
|
19248
19416
|
for (const file of plan) {
|
|
19249
|
-
const dest =
|
|
19417
|
+
const dest = path27.join(root, file.rel);
|
|
19250
19418
|
if (!opts.force && await exists4(dest)) {
|
|
19251
19419
|
skipped++;
|
|
19252
19420
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
19253
19421
|
continue;
|
|
19254
19422
|
}
|
|
19255
|
-
await
|
|
19256
|
-
await
|
|
19423
|
+
await fs27.mkdir(path27.dirname(dest), { recursive: true });
|
|
19424
|
+
await fs27.copyFile(path27.join(srcDir, file.from), dest);
|
|
19257
19425
|
copied++;
|
|
19258
19426
|
log.dim(` ${file.rel}`);
|
|
19259
19427
|
}
|
|
@@ -19306,7 +19474,7 @@ async function runController(opts) {
|
|
|
19306
19474
|
for (const line of set.sketch) {
|
|
19307
19475
|
log.dim(` ${line}`);
|
|
19308
19476
|
}
|
|
19309
|
-
if (kind === "character" && !await exists4(
|
|
19477
|
+
if (kind === "character" && !await exists4(path27.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
19310
19478
|
log.plain("");
|
|
19311
19479
|
log.plain(
|
|
19312
19480
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -19338,9 +19506,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
19338
19506
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
19339
19507
|
}
|
|
19340
19508
|
assertCompleteMeshyControllerPack(manifest);
|
|
19341
|
-
const destination =
|
|
19342
|
-
await
|
|
19343
|
-
await
|
|
19509
|
+
const destination = path27.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
19510
|
+
await fs27.mkdir(path27.dirname(destination), { recursive: true });
|
|
19511
|
+
await fs27.writeFile(
|
|
19344
19512
|
destination,
|
|
19345
19513
|
`${JSON.stringify(manifest, null, 2)}
|
|
19346
19514
|
`
|
|
@@ -19485,14 +19653,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
19485
19653
|
}
|
|
19486
19654
|
async function installFallbackAvatar(args) {
|
|
19487
19655
|
const { root, srcDir, log } = args;
|
|
19488
|
-
const dest =
|
|
19489
|
-
await
|
|
19490
|
-
await
|
|
19656
|
+
const dest = path27.join(root, ASSETS_DEST, "avatar.vrm");
|
|
19657
|
+
await fs27.mkdir(path27.dirname(dest), { recursive: true });
|
|
19658
|
+
await fs27.copyFile(path27.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
19491
19659
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
19492
19660
|
}
|
|
19493
19661
|
async function exists4(p) {
|
|
19494
19662
|
try {
|
|
19495
|
-
await
|
|
19663
|
+
await fs27.access(p);
|
|
19496
19664
|
return true;
|
|
19497
19665
|
} catch {
|
|
19498
19666
|
return false;
|
|
@@ -19500,8 +19668,8 @@ async function exists4(p) {
|
|
|
19500
19668
|
}
|
|
19501
19669
|
|
|
19502
19670
|
// src/commands/character.ts
|
|
19503
|
-
import
|
|
19504
|
-
import
|
|
19671
|
+
import fs28 from "fs/promises";
|
|
19672
|
+
import path28 from "path";
|
|
19505
19673
|
function exactAnimation(selector) {
|
|
19506
19674
|
const trimmed = selector.trim();
|
|
19507
19675
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -19581,7 +19749,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
19581
19749
|
}
|
|
19582
19750
|
process.exitCode = 1;
|
|
19583
19751
|
}
|
|
19584
|
-
var INSTALLED_MANIFEST =
|
|
19752
|
+
var INSTALLED_MANIFEST = path28.join("public", "assets", "meshy-character.json");
|
|
19585
19753
|
async function resolveAdoptTarget(selector) {
|
|
19586
19754
|
const trimmed = selector?.trim();
|
|
19587
19755
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -19590,7 +19758,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
19590
19758
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
19591
19759
|
let raw;
|
|
19592
19760
|
try {
|
|
19593
|
-
raw = await
|
|
19761
|
+
raw = await fs28.readFile(file, "utf8");
|
|
19594
19762
|
} catch {
|
|
19595
19763
|
return {
|
|
19596
19764
|
ok: false,
|
|
@@ -19650,6 +19818,8 @@ async function runCharacterAdopt(opts) {
|
|
|
19650
19818
|
createPath: `/api/characters/${encodeURIComponent(target.characterId)}/adopt`,
|
|
19651
19819
|
body: { projectId: project.id },
|
|
19652
19820
|
quote: { credits: 0 },
|
|
19821
|
+
// A copy, not a generation: it costs nothing, and the allowance sees 0.
|
|
19822
|
+
quotedCredits: 0,
|
|
19653
19823
|
completed: (view) => {
|
|
19654
19824
|
if (opts.json) {
|
|
19655
19825
|
writeJson({
|
|
@@ -19711,10 +19881,11 @@ async function runCharacterImport(opts) {
|
|
|
19711
19881
|
opts,
|
|
19712
19882
|
ctx,
|
|
19713
19883
|
kind: "character",
|
|
19714
|
-
prompt: `Import ${
|
|
19884
|
+
prompt: `Import ${path28.basename(filePath)} as a rigged character`,
|
|
19715
19885
|
createPath: "/api/characters/import",
|
|
19716
19886
|
body,
|
|
19717
19887
|
quote: price,
|
|
19888
|
+
quotedCredits: price.credits,
|
|
19718
19889
|
completed: (view) => {
|
|
19719
19890
|
const confidence = typeof view.metadata?.autoRigConfidence === "number" ? view.metadata.autoRigConfidence : null;
|
|
19720
19891
|
const next = {
|
|
@@ -19789,12 +19960,28 @@ async function runWorkflow(args) {
|
|
|
19789
19960
|
return;
|
|
19790
19961
|
}
|
|
19791
19962
|
}
|
|
19963
|
+
{
|
|
19964
|
+
const budget = await assetBudgetGate({
|
|
19965
|
+
apiUrl: args.ctx.apiUrl,
|
|
19966
|
+
token: args.ctx.token,
|
|
19967
|
+
kind: args.kind,
|
|
19968
|
+
quotedCredits: args.quotedCredits,
|
|
19969
|
+
log
|
|
19970
|
+
});
|
|
19971
|
+
if (!budget.allow) {
|
|
19972
|
+
log.error(budget.reason);
|
|
19973
|
+
process.exitCode = 1;
|
|
19974
|
+
return;
|
|
19975
|
+
}
|
|
19976
|
+
}
|
|
19792
19977
|
const created = await postWorkflow(`${args.ctx.apiUrl}${args.createPath}`, args.ctx.token, args.body);
|
|
19793
19978
|
if (!created) {
|
|
19794
19979
|
process.exitCode = 1;
|
|
19795
19980
|
return;
|
|
19796
19981
|
}
|
|
19797
|
-
await recordQueued(created.id, args.kind, args.prompt
|
|
19982
|
+
await recordQueued(created.id, args.kind, args.prompt, void 0, {
|
|
19983
|
+
credits: created.creditsQuoted ?? args.quotedCredits
|
|
19984
|
+
});
|
|
19798
19985
|
if (args.opts.noWait) {
|
|
19799
19986
|
const nextCommand = `genex wait ${created.id}${args.opts.json ? " --json" : ""}`;
|
|
19800
19987
|
if (args.opts.json) {
|
|
@@ -19894,6 +20081,7 @@ async function runCharacterConcept(opts) {
|
|
|
19894
20081
|
createPath: "/api/characters/concepts",
|
|
19895
20082
|
body: request,
|
|
19896
20083
|
quote: price,
|
|
20084
|
+
quotedCredits: price.credits,
|
|
19897
20085
|
completed: (view) => reportCharacterConceptTerminal(view, log, {
|
|
19898
20086
|
json: opts.json,
|
|
19899
20087
|
open: opts.open,
|
|
@@ -19945,6 +20133,7 @@ async function runDirectTextCharacter(opts) {
|
|
|
19945
20133
|
prompt,
|
|
19946
20134
|
token: ctx.token,
|
|
19947
20135
|
apiUrl: ctx.apiUrl,
|
|
20136
|
+
quotedCredits: price.credits,
|
|
19948
20137
|
generationOptions: {
|
|
19949
20138
|
actionIds: resolved.actionIds,
|
|
19950
20139
|
controllerPack,
|
|
@@ -20014,6 +20203,7 @@ async function runCharacterPreview(opts) {
|
|
|
20014
20203
|
createPath: basePath,
|
|
20015
20204
|
body,
|
|
20016
20205
|
quote: price,
|
|
20206
|
+
quotedCredits: price.credits,
|
|
20017
20207
|
completed: (view) => reportCharacterPreviewTerminal(view, log, {
|
|
20018
20208
|
json: opts.json,
|
|
20019
20209
|
open: opts.open,
|
|
@@ -20087,6 +20277,7 @@ async function runCharacterFinalize(opts) {
|
|
|
20087
20277
|
createPath: basePath,
|
|
20088
20278
|
body,
|
|
20089
20279
|
quote: price,
|
|
20280
|
+
quotedCredits: price.credits,
|
|
20090
20281
|
completed: async (view, local) => {
|
|
20091
20282
|
if (opts.json) {
|
|
20092
20283
|
const files = view.files ?? [];
|
|
@@ -20176,6 +20367,7 @@ async function runCharacterAnimate(opts) {
|
|
|
20176
20367
|
prompt: `Attach Meshy actions ${resolved.actionIds.join(", ")} to character ${characterId}`,
|
|
20177
20368
|
token: ctx.token,
|
|
20178
20369
|
apiUrl: ctx.apiUrl,
|
|
20370
|
+
quotedCredits: price.credits,
|
|
20179
20371
|
generationOptions: { characterId, actionIds: resolved.actionIds }
|
|
20180
20372
|
});
|
|
20181
20373
|
}
|
|
@@ -20191,22 +20383,22 @@ async function context2(opts) {
|
|
|
20191
20383
|
const project = await readProject();
|
|
20192
20384
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
20193
20385
|
}
|
|
20194
|
-
async function readVideo(
|
|
20386
|
+
async function readVideo(path35, log) {
|
|
20195
20387
|
let bytes;
|
|
20196
20388
|
try {
|
|
20197
|
-
bytes = await readFile(
|
|
20389
|
+
bytes = await readFile(path35);
|
|
20198
20390
|
} catch {
|
|
20199
|
-
log.error(`Can't read ${
|
|
20391
|
+
log.error(`Can't read ${path35}.`);
|
|
20200
20392
|
return null;
|
|
20201
20393
|
}
|
|
20202
20394
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
20203
|
-
log.error(`${basename(
|
|
20395
|
+
log.error(`${basename(path35)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
20204
20396
|
return null;
|
|
20205
20397
|
}
|
|
20206
20398
|
return bytes;
|
|
20207
20399
|
}
|
|
20208
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
20209
|
-
const contentType = /\.mov$/i.test(
|
|
20400
|
+
async function uploadVideo(apiUrl, token, characterId, path35, bytes, log) {
|
|
20401
|
+
const contentType = /\.mov$/i.test(path35) ? "video/quicktime" : "video/mp4";
|
|
20210
20402
|
const minted = await apiFetch(
|
|
20211
20403
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
20212
20404
|
{
|
|
@@ -20221,7 +20413,7 @@ async function uploadVideo(apiUrl, token, characterId, path33, bytes, log) {
|
|
|
20221
20413
|
return null;
|
|
20222
20414
|
}
|
|
20223
20415
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
20224
|
-
log.dim(` uploading ${basename(
|
|
20416
|
+
log.dim(` uploading ${basename(path35)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
20225
20417
|
const put = await fetch(uploadUrl, {
|
|
20226
20418
|
method: "PUT",
|
|
20227
20419
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -20537,8 +20729,8 @@ function rank(items, query) {
|
|
|
20537
20729
|
}
|
|
20538
20730
|
|
|
20539
20731
|
// src/commands/motion.ts
|
|
20540
|
-
import
|
|
20541
|
-
import
|
|
20732
|
+
import fs29 from "fs/promises";
|
|
20733
|
+
import path29 from "path";
|
|
20542
20734
|
|
|
20543
20735
|
// src/lib/motion/npz.ts
|
|
20544
20736
|
import zlib from "zlib";
|
|
@@ -21789,7 +21981,7 @@ async function motionGen(opts, log) {
|
|
|
21789
21981
|
}
|
|
21790
21982
|
if (opts.constraintsPath !== void 0) {
|
|
21791
21983
|
try {
|
|
21792
|
-
const raw = await
|
|
21984
|
+
const raw = await fs29.readFile(opts.constraintsPath, "utf8");
|
|
21793
21985
|
generationOptions.constraints = JSON.parse(raw);
|
|
21794
21986
|
} catch {
|
|
21795
21987
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -21813,10 +22005,10 @@ async function motionGen(opts, log) {
|
|
|
21813
22005
|
async function expandTakes(selectors) {
|
|
21814
22006
|
const out = [];
|
|
21815
22007
|
for (const sel of selectors) {
|
|
21816
|
-
const st = await
|
|
22008
|
+
const st = await fs29.stat(sel).catch(() => null);
|
|
21817
22009
|
if (st?.isDirectory()) {
|
|
21818
|
-
const names = await
|
|
21819
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
22010
|
+
const names = await fs29.readdir(sel);
|
|
22011
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path29.join(sel, n));
|
|
21820
22012
|
} else if (st?.isFile()) {
|
|
21821
22013
|
out.push(sel);
|
|
21822
22014
|
} else {
|
|
@@ -21851,7 +22043,7 @@ async function motionVerify(opts, log) {
|
|
|
21851
22043
|
let gates = DEFAULT_GATES;
|
|
21852
22044
|
if (opts.gatesPath) {
|
|
21853
22045
|
try {
|
|
21854
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
22046
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs29.readFile(opts.gatesPath, "utf8")));
|
|
21855
22047
|
} catch {
|
|
21856
22048
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
21857
22049
|
process.exitCode = 1;
|
|
@@ -21873,9 +22065,9 @@ async function motionVerify(opts, log) {
|
|
|
21873
22065
|
}
|
|
21874
22066
|
const reports = [];
|
|
21875
22067
|
for (const file of files) {
|
|
21876
|
-
const stem =
|
|
22068
|
+
const stem = path29.basename(file).replace(/\.npz$/, "");
|
|
21877
22069
|
try {
|
|
21878
|
-
reports.push(analyzeTake(stem, await
|
|
22070
|
+
reports.push(analyzeTake(stem, await fs29.readFile(file), gates));
|
|
21879
22071
|
} catch (err) {
|
|
21880
22072
|
reports.push({
|
|
21881
22073
|
take: stem,
|
|
@@ -21913,7 +22105,7 @@ async function motionCompile(opts, log) {
|
|
|
21913
22105
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
21914
22106
|
if (opts.configPath) {
|
|
21915
22107
|
try {
|
|
21916
|
-
const patch = JSON.parse(await
|
|
22108
|
+
const patch = JSON.parse(await fs29.readFile(opts.configPath, "utf8"));
|
|
21917
22109
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
21918
22110
|
} catch {
|
|
21919
22111
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -21931,16 +22123,16 @@ async function motionCompile(opts, log) {
|
|
|
21931
22123
|
}
|
|
21932
22124
|
const inputs = [];
|
|
21933
22125
|
for (const file of files) {
|
|
21934
|
-
const stem =
|
|
22126
|
+
const stem = path29.basename(file).replace(/\.npz$/, "");
|
|
21935
22127
|
try {
|
|
21936
|
-
inputs.push({ stem, take: loadTake(await
|
|
22128
|
+
inputs.push({ stem, take: loadTake(await fs29.readFile(file)) });
|
|
21937
22129
|
} catch (err) {
|
|
21938
22130
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
21939
22131
|
process.exitCode = 1;
|
|
21940
22132
|
return;
|
|
21941
22133
|
}
|
|
21942
22134
|
}
|
|
21943
|
-
const setName = opts.set ??
|
|
22135
|
+
const setName = opts.set ?? path29.basename(opts.out).replace(/\.json$/, "");
|
|
21944
22136
|
let result;
|
|
21945
22137
|
try {
|
|
21946
22138
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -21955,9 +22147,9 @@ async function motionCompile(opts, log) {
|
|
|
21955
22147
|
process.exitCode = 1;
|
|
21956
22148
|
return;
|
|
21957
22149
|
}
|
|
21958
|
-
await
|
|
22150
|
+
await fs29.mkdir(path29.dirname(path29.resolve(opts.out)), { recursive: true });
|
|
21959
22151
|
const json = JSON.stringify(result.data);
|
|
21960
|
-
await
|
|
22152
|
+
await fs29.writeFile(opts.out, json);
|
|
21961
22153
|
if (opts.json) {
|
|
21962
22154
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
21963
22155
|
return;
|
|
@@ -21975,9 +22167,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
21975
22167
|
var MOTION_PRESETS = {
|
|
21976
22168
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
21977
22169
|
};
|
|
21978
|
-
var MOTION_DEST =
|
|
22170
|
+
var MOTION_DEST = path29.join("src", "motion");
|
|
21979
22171
|
async function motionInstall(opts, log) {
|
|
21980
|
-
const srcDir =
|
|
22172
|
+
const srcDir = path29.join(getTemplatesDir(), "motion");
|
|
21981
22173
|
const root = opts.cwd ?? process.cwd();
|
|
21982
22174
|
const preset = opts.set;
|
|
21983
22175
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -21988,21 +22180,21 @@ async function motionInstall(opts, log) {
|
|
|
21988
22180
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
21989
22181
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
21990
22182
|
log.plain("");
|
|
21991
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
22183
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path29.sep)}`);
|
|
21992
22184
|
let copied = 0, skipped = 0;
|
|
21993
22185
|
try {
|
|
21994
22186
|
for (const rel of files) {
|
|
21995
|
-
const dest =
|
|
21996
|
-
const exists5 = await
|
|
22187
|
+
const dest = path29.join(root, MOTION_DEST, rel);
|
|
22188
|
+
const exists5 = await fs29.access(dest).then(() => true, () => false);
|
|
21997
22189
|
if (!opts.force && exists5) {
|
|
21998
22190
|
skipped++;
|
|
21999
|
-
log.dim(` skipped ${
|
|
22191
|
+
log.dim(` skipped ${path29.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
22000
22192
|
continue;
|
|
22001
22193
|
}
|
|
22002
|
-
await
|
|
22003
|
-
await
|
|
22194
|
+
await fs29.mkdir(path29.dirname(dest), { recursive: true });
|
|
22195
|
+
await fs29.copyFile(path29.join(srcDir, rel), dest);
|
|
22004
22196
|
copied++;
|
|
22005
|
-
log.dim(` ${
|
|
22197
|
+
log.dim(` ${path29.join(MOTION_DEST, rel)}`);
|
|
22006
22198
|
}
|
|
22007
22199
|
} catch (err) {
|
|
22008
22200
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -22043,7 +22235,7 @@ async function motionConstraints(opts, log) {
|
|
|
22043
22235
|
}
|
|
22044
22236
|
const doc = directionConstraint(dir, speed, duration);
|
|
22045
22237
|
const out = opts.out ?? "constraints.json";
|
|
22046
|
-
await
|
|
22238
|
+
await fs29.writeFile(out, JSON.stringify(doc));
|
|
22047
22239
|
if (opts.json) {
|
|
22048
22240
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
22049
22241
|
return;
|
|
@@ -22080,14 +22272,14 @@ async function runMotion(opts) {
|
|
|
22080
22272
|
}
|
|
22081
22273
|
|
|
22082
22274
|
// src/commands/blender.ts
|
|
22083
|
-
import
|
|
22084
|
-
import
|
|
22275
|
+
import fs30 from "fs/promises";
|
|
22276
|
+
import path30 from "path";
|
|
22085
22277
|
var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
|
|
22086
22278
|
var DEFAULT_OUT_DIR = "assets/blender";
|
|
22087
22279
|
async function writeB64(dir, name, b64) {
|
|
22088
|
-
await
|
|
22089
|
-
const p =
|
|
22090
|
-
await
|
|
22280
|
+
await fs30.mkdir(dir, { recursive: true });
|
|
22281
|
+
const p = path30.join(dir, name);
|
|
22282
|
+
await fs30.writeFile(p, Buffer.from(b64, "base64"));
|
|
22091
22283
|
return p;
|
|
22092
22284
|
}
|
|
22093
22285
|
function reportScene(log, s) {
|
|
@@ -22104,7 +22296,7 @@ async function runBlender(opts) {
|
|
|
22104
22296
|
return 1;
|
|
22105
22297
|
}
|
|
22106
22298
|
if (sub === "serve") {
|
|
22107
|
-
const { serveLocalBlender } = await import("./blender-serve-
|
|
22299
|
+
const { serveLocalBlender } = await import("./blender-serve-BF4FZ55Z.js");
|
|
22108
22300
|
const port = Number(process.env.GENEX_BLENDER_PORT ?? 8088);
|
|
22109
22301
|
log.step(`Starting a local Blender service on port ${port}`);
|
|
22110
22302
|
log.plain(
|
|
@@ -22113,7 +22305,7 @@ async function runBlender(opts) {
|
|
|
22113
22305
|
return serveLocalBlender({ port, log });
|
|
22114
22306
|
}
|
|
22115
22307
|
if (sub === "mcp") {
|
|
22116
|
-
const { runBlenderMcp } = await import("./blender-mcp-
|
|
22308
|
+
const { runBlenderMcp } = await import("./blender-mcp-Q6PSFYSE.js");
|
|
22117
22309
|
return runBlenderMcp();
|
|
22118
22310
|
}
|
|
22119
22311
|
if (sub === "seat") {
|
|
@@ -22171,7 +22363,7 @@ async function runBlender(opts) {
|
|
|
22171
22363
|
log.plain(rest.join("\n"));
|
|
22172
22364
|
return 1;
|
|
22173
22365
|
}
|
|
22174
|
-
const outDir =
|
|
22366
|
+
const outDir = path30.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
|
|
22175
22367
|
const mode = opts.mode;
|
|
22176
22368
|
if (mode !== void 0 && !isRenderMode(mode)) {
|
|
22177
22369
|
log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
|
|
@@ -22211,14 +22403,14 @@ async function runBlender(opts) {
|
|
|
22211
22403
|
return 0;
|
|
22212
22404
|
}
|
|
22213
22405
|
case "export": {
|
|
22214
|
-
const target = opts.out ??
|
|
22406
|
+
const target = opts.out ?? path30.join(outDir, "scene.glb");
|
|
22215
22407
|
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
22216
22408
|
if (!r.glbBase64) {
|
|
22217
22409
|
log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
|
|
22218
22410
|
return 1;
|
|
22219
22411
|
}
|
|
22220
|
-
await
|
|
22221
|
-
await
|
|
22412
|
+
await fs30.mkdir(path30.dirname(target), { recursive: true });
|
|
22413
|
+
await fs30.writeFile(target, Buffer.from(r.glbBase64, "base64"));
|
|
22222
22414
|
log.success(`Exported ${r.bytes ?? 0} bytes`);
|
|
22223
22415
|
log.plain(` ${c.cyan(target)}`);
|
|
22224
22416
|
return 0;
|
|
@@ -22251,12 +22443,12 @@ async function runBlender(opts) {
|
|
|
22251
22443
|
return 1;
|
|
22252
22444
|
}
|
|
22253
22445
|
try {
|
|
22254
|
-
script = await
|
|
22446
|
+
script = await fs30.readFile(opts.input, "utf8");
|
|
22255
22447
|
} catch {
|
|
22256
22448
|
log.error(`Can't read ${opts.input}`);
|
|
22257
22449
|
return 1;
|
|
22258
22450
|
}
|
|
22259
|
-
label =
|
|
22451
|
+
label = path30.basename(opts.input);
|
|
22260
22452
|
}
|
|
22261
22453
|
const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
|
|
22262
22454
|
if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
|
|
@@ -22375,9 +22567,9 @@ print(f"castle: {n} objects")
|
|
|
22375
22567
|
`;
|
|
22376
22568
|
|
|
22377
22569
|
// src/commands/asset-new.ts
|
|
22378
|
-
import
|
|
22570
|
+
import fs31 from "fs";
|
|
22379
22571
|
import fsp from "fs/promises";
|
|
22380
|
-
import
|
|
22572
|
+
import path31 from "path";
|
|
22381
22573
|
import { pathToFileURL } from "url";
|
|
22382
22574
|
var EXTRA_FILES = [
|
|
22383
22575
|
"genex-asset.example.json",
|
|
@@ -22471,7 +22663,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
22471
22663
|
}
|
|
22472
22664
|
async function runAssetNew(options) {
|
|
22473
22665
|
const log = createLogger();
|
|
22474
|
-
const cwd = options.dir ?
|
|
22666
|
+
const cwd = options.dir ? path31.resolve(options.dir) : process.cwd();
|
|
22475
22667
|
const slug = options.assetSlug;
|
|
22476
22668
|
if (!slug) {
|
|
22477
22669
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -22481,14 +22673,14 @@ async function runAssetNew(options) {
|
|
|
22481
22673
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
22482
22674
|
return 1;
|
|
22483
22675
|
}
|
|
22484
|
-
const templateDir =
|
|
22485
|
-
if (!
|
|
22676
|
+
const templateDir = path31.join(getTemplatesDir(), "asset-viewer");
|
|
22677
|
+
if (!fs31.existsSync(templateDir)) {
|
|
22486
22678
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
22487
22679
|
return 1;
|
|
22488
22680
|
}
|
|
22489
|
-
const manifestTools = await import(pathToFileURL(
|
|
22681
|
+
const manifestTools = await import(pathToFileURL(path31.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
22490
22682
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
22491
|
-
const lockPath =
|
|
22683
|
+
const lockPath = path31.join(templateDir, "shared-files.sha256.json");
|
|
22492
22684
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
22493
22685
|
const actual = hashSharedFiles(templateDir);
|
|
22494
22686
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -22501,8 +22693,8 @@ async function runAssetNew(options) {
|
|
|
22501
22693
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
22502
22694
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
22503
22695
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
22504
|
-
const outDir =
|
|
22505
|
-
if (
|
|
22696
|
+
const outDir = path31.resolve(cwd, options.out ?? slug);
|
|
22697
|
+
if (fs31.existsSync(outDir) && fs31.readdirSync(outDir).length > 0 && !options.force) {
|
|
22506
22698
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
22507
22699
|
return 1;
|
|
22508
22700
|
}
|
|
@@ -22530,24 +22722,24 @@ async function runAssetNew(options) {
|
|
|
22530
22722
|
};
|
|
22531
22723
|
await fsp.mkdir(outDir, { recursive: true });
|
|
22532
22724
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
22533
|
-
const to =
|
|
22534
|
-
await fsp.mkdir(
|
|
22535
|
-
await fsp.copyFile(
|
|
22725
|
+
const to = path31.join(outDir, rel);
|
|
22726
|
+
await fsp.mkdir(path31.dirname(to), { recursive: true });
|
|
22727
|
+
await fsp.copyFile(path31.join(templateDir, rel), to);
|
|
22536
22728
|
}
|
|
22537
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
22729
|
+
const pkg = fillTemplate(await fsp.readFile(path31.join(templateDir, "package.json"), "utf8"), {
|
|
22538
22730
|
slug,
|
|
22539
22731
|
name,
|
|
22540
22732
|
version
|
|
22541
22733
|
});
|
|
22542
|
-
await fsp.writeFile(
|
|
22543
|
-
await fsp.writeFile(
|
|
22544
|
-
await fsp.writeFile(
|
|
22734
|
+
await fsp.writeFile(path31.join(outDir, "package.json"), pkg, "utf8");
|
|
22735
|
+
await fsp.writeFile(path31.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
22736
|
+
await fsp.writeFile(path31.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
22545
22737
|
await fsp.writeFile(
|
|
22546
|
-
|
|
22738
|
+
path31.join(outDir, "DESIGN.md"),
|
|
22547
22739
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
22548
22740
|
"utf8"
|
|
22549
22741
|
);
|
|
22550
|
-
const placeholder = await fsp.readFile(
|
|
22742
|
+
const placeholder = await fsp.readFile(path31.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
22551
22743
|
const seeded = seedAssetSource(placeholder, {
|
|
22552
22744
|
slug,
|
|
22553
22745
|
name,
|
|
@@ -22558,8 +22750,8 @@ async function runAssetNew(options) {
|
|
|
22558
22750
|
pascalCase
|
|
22559
22751
|
});
|
|
22560
22752
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
22561
|
-
await fsp.mkdir(
|
|
22562
|
-
await fsp.writeFile(
|
|
22753
|
+
await fsp.mkdir(path31.join(outDir, "src", "asset"), { recursive: true });
|
|
22754
|
+
await fsp.writeFile(path31.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
22563
22755
|
const copied = hashSharedFiles(outDir);
|
|
22564
22756
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
22565
22757
|
if (mismatched.length) {
|
|
@@ -22567,7 +22759,7 @@ async function runAssetNew(options) {
|
|
|
22567
22759
|
return 1;
|
|
22568
22760
|
}
|
|
22569
22761
|
await fsp.writeFile(
|
|
22570
|
-
|
|
22762
|
+
path31.join(outDir, PARITY_FILENAME),
|
|
22571
22763
|
JSON.stringify(
|
|
22572
22764
|
{
|
|
22573
22765
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -22611,10 +22803,10 @@ async function runAssetNew(options) {
|
|
|
22611
22803
|
}
|
|
22612
22804
|
|
|
22613
22805
|
// src/commands/tools.ts
|
|
22614
|
-
import
|
|
22806
|
+
import path33 from "path";
|
|
22615
22807
|
|
|
22616
22808
|
// src/commands/doctor.ts
|
|
22617
|
-
import
|
|
22809
|
+
import path32 from "path";
|
|
22618
22810
|
var LANE_ORDER = [
|
|
22619
22811
|
"model",
|
|
22620
22812
|
"image",
|
|
@@ -22895,7 +23087,7 @@ async function fetchLegalStatus(apiUrl, token) {
|
|
|
22895
23087
|
}
|
|
22896
23088
|
async function firstSkillsMarker() {
|
|
22897
23089
|
for (const target of resolveAgentTargets()) {
|
|
22898
|
-
const marker = await readSkillsMarker(
|
|
23090
|
+
const marker = await readSkillsMarker(path32.join(target.baseDir, "skills"));
|
|
22899
23091
|
if (marker) return marker;
|
|
22900
23092
|
}
|
|
22901
23093
|
return null;
|
|
@@ -22946,8 +23138,8 @@ async function runTools(opts) {
|
|
|
22946
23138
|
let totalNew = 0;
|
|
22947
23139
|
let totalUpdated = 0;
|
|
22948
23140
|
for (const t of targets) {
|
|
22949
|
-
const dest =
|
|
22950
|
-
const { copied, updated } = await copyTemplates(
|
|
23141
|
+
const dest = path33.join(t.baseDir, "skills");
|
|
23142
|
+
const { copied, updated } = await copyTemplates(path33.join(templatesDir, "skills"), dest, {
|
|
22951
23143
|
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
22952
23144
|
});
|
|
22953
23145
|
await pruneRemovedSkills(dest, log);
|
|
@@ -23005,6 +23197,164 @@ async function runTools(opts) {
|
|
|
23005
23197
|
log.dim(" Assets land in ./assets/ as files you own \u2014 wire the local path into your game.");
|
|
23006
23198
|
}
|
|
23007
23199
|
|
|
23200
|
+
// src/commands/budget.ts
|
|
23201
|
+
import fs32 from "fs/promises";
|
|
23202
|
+
import path34 from "path";
|
|
23203
|
+
var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
|
|
23204
|
+
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.";
|
|
23205
|
+
function internalKind(kind) {
|
|
23206
|
+
return kind === "agent_session" || kind === "blender_seat" || kind === "avatar_look" || kind.startsWith("outfit") || kind.startsWith("cover_");
|
|
23207
|
+
}
|
|
23208
|
+
async function isGenexProject(cwd) {
|
|
23209
|
+
try {
|
|
23210
|
+
await fs32.access(path34.join(cwd, ".genex"));
|
|
23211
|
+
return true;
|
|
23212
|
+
} catch {
|
|
23213
|
+
return false;
|
|
23214
|
+
}
|
|
23215
|
+
}
|
|
23216
|
+
function runnableKinds(prices) {
|
|
23217
|
+
return Object.entries(prices).filter(([kind, credits]) => !internalKind(kind) && typeof credits === "number" && Number.isFinite(credits)).sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0]));
|
|
23218
|
+
}
|
|
23219
|
+
function describeDefault(config) {
|
|
23220
|
+
const share = config.share === 0.5 ? "half" : `${Math.round(config.share * 100)}%`;
|
|
23221
|
+
return `default: ${share} of what was spendable at the first paid command, capped at ${config.cap}`;
|
|
23222
|
+
}
|
|
23223
|
+
function isoDay(iso) {
|
|
23224
|
+
const d = new Date(iso);
|
|
23225
|
+
return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10);
|
|
23226
|
+
}
|
|
23227
|
+
async function runBudget(opts = {}) {
|
|
23228
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
23229
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
23230
|
+
const config = budgetConfig();
|
|
23231
|
+
const fail3 = (message) => {
|
|
23232
|
+
if (opts.json) writeJson({ command: "budget", status: "failed", error: message });
|
|
23233
|
+
else log.error(message);
|
|
23234
|
+
process.exitCode = 1;
|
|
23235
|
+
};
|
|
23236
|
+
const setting = opts.assets !== void 0;
|
|
23237
|
+
if (setting && opts.userApproved !== true) {
|
|
23238
|
+
fail3(BUDGET_APPROVAL_REQUIRED);
|
|
23239
|
+
return;
|
|
23240
|
+
}
|
|
23241
|
+
if (setting && (!Number.isInteger(opts.assets) || opts.assets < 0)) {
|
|
23242
|
+
fail3(`--assets takes a whole number of credits, 0 or more (got ${String(opts.assets)}).`);
|
|
23243
|
+
return;
|
|
23244
|
+
}
|
|
23245
|
+
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
23246
|
+
if (!token) {
|
|
23247
|
+
fail3("Not authorized. Run `genex init` first to sign in.");
|
|
23248
|
+
return;
|
|
23249
|
+
}
|
|
23250
|
+
const meta = await readProject(cwd);
|
|
23251
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
|
|
23252
|
+
const snapshot = await fetchCreditsSnapshot(apiUrl, token);
|
|
23253
|
+
let store;
|
|
23254
|
+
let persisted = true;
|
|
23255
|
+
if (setting) {
|
|
23256
|
+
const previous = await readBudgetStore(cwd);
|
|
23257
|
+
store = {
|
|
23258
|
+
v: 1,
|
|
23259
|
+
allowanceCredits: opts.assets,
|
|
23260
|
+
setBy: "user",
|
|
23261
|
+
setAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23262
|
+
spendableAtStart: previous?.spendableAtStart ?? snapshot?.spendable ?? 0,
|
|
23263
|
+
balanceAtStart: previous?.balanceAtStart ?? snapshot?.balance ?? 0
|
|
23264
|
+
};
|
|
23265
|
+
persisted = await writeBudgetStore(cwd, store);
|
|
23266
|
+
if (!persisted) {
|
|
23267
|
+
fail3("This folder is not a Genex project (no .genex/), so the allowance has nowhere to live. Run this inside the game folder.");
|
|
23268
|
+
return;
|
|
23269
|
+
}
|
|
23270
|
+
} else if (snapshot && !snapshot.unlimited && !budgetDisabled(config)) {
|
|
23271
|
+
store = await ensureBudgetStore(cwd, {
|
|
23272
|
+
balance: snapshot.balance,
|
|
23273
|
+
spendable: snapshot.spendable,
|
|
23274
|
+
config
|
|
23275
|
+
});
|
|
23276
|
+
persisted = await readBudgetStore(cwd) !== null;
|
|
23277
|
+
} else {
|
|
23278
|
+
store = await readBudgetStore(cwd);
|
|
23279
|
+
persisted = await isGenexProject(cwd);
|
|
23280
|
+
}
|
|
23281
|
+
const spent = spentCredits(await readLedgerRows(cwd));
|
|
23282
|
+
const disabled = budgetDisabled(config);
|
|
23283
|
+
const enforced = !disabled && !(snapshot?.unlimited ?? false);
|
|
23284
|
+
const allowance = store?.allowanceCredits ?? null;
|
|
23285
|
+
const remaining = allowance === null ? null : Math.max(0, allowance - spent);
|
|
23286
|
+
if (opts.json) {
|
|
23287
|
+
writeJson({
|
|
23288
|
+
command: "budget",
|
|
23289
|
+
...snapshot ? {
|
|
23290
|
+
balance: snapshot.balance,
|
|
23291
|
+
reserved: snapshot.reserved,
|
|
23292
|
+
spendable: snapshot.spendable,
|
|
23293
|
+
unlimited: snapshot.unlimited
|
|
23294
|
+
} : { balance: null, reserved: null, spendable: null, unlimited: null },
|
|
23295
|
+
allowance: {
|
|
23296
|
+
credits: allowance,
|
|
23297
|
+
setBy: store?.setBy ?? null,
|
|
23298
|
+
setAt: store?.setAt ?? null,
|
|
23299
|
+
spendableAtStart: store?.spendableAtStart ?? null,
|
|
23300
|
+
balanceAtStart: store?.balanceAtStart ?? null,
|
|
23301
|
+
enforced,
|
|
23302
|
+
disabled,
|
|
23303
|
+
persisted
|
|
23304
|
+
},
|
|
23305
|
+
spent,
|
|
23306
|
+
remaining,
|
|
23307
|
+
prices: snapshot ? Object.fromEntries(runnableKinds(snapshot.prices)) : null,
|
|
23308
|
+
source: BUDGET_SOURCE_LINE,
|
|
23309
|
+
...snapshot ? {} : { warning: `Balance and prices couldn't be read from ${apiUrl}.` }
|
|
23310
|
+
});
|
|
23311
|
+
if (!snapshot && !setting) process.exitCode = 1;
|
|
23312
|
+
return;
|
|
23313
|
+
}
|
|
23314
|
+
log.plain(c.bold("genex budget"));
|
|
23315
|
+
if (setting && store) {
|
|
23316
|
+
log.success(`Asset allowance set to ${store.allowanceCredits} credits (approved by the player).`);
|
|
23317
|
+
}
|
|
23318
|
+
if (snapshot) {
|
|
23319
|
+
log.plain(` Balance ${snapshot.balance} credits \xB7 ${snapshot.reserved} reserved \xB7 ${snapshot.spendable} spendable`);
|
|
23320
|
+
} else {
|
|
23321
|
+
log.warn(` Balance couldn't be read from ${apiUrl} \u2014 run ${c.cyan("npx genex doctor")}.`);
|
|
23322
|
+
}
|
|
23323
|
+
if (disabled) {
|
|
23324
|
+
log.plain(` Allowance off (GENEX_ASSET_BUDGET_CAP=0)`);
|
|
23325
|
+
} else if (snapshot?.unlimited) {
|
|
23326
|
+
log.plain(` Allowance not enforced (unlimited operator account)`);
|
|
23327
|
+
} else if (store) {
|
|
23328
|
+
const origin = store.setBy === "user" ? `set by you on ${isoDay(store.setAt)}` : describeDefault(config);
|
|
23329
|
+
log.plain(` Allowance ${store.allowanceCredits} credits (${origin})`);
|
|
23330
|
+
} else {
|
|
23331
|
+
log.plain(` Allowance not derived yet \u2014 it is sized from the wallet at the first paid command`);
|
|
23332
|
+
}
|
|
23333
|
+
log.plain(` Spent ${spent} credits this build (quoted; failed generations are refunded and excluded)`);
|
|
23334
|
+
if (remaining !== null && enforced) {
|
|
23335
|
+
log.plain(` Remaining ${remaining} credits`);
|
|
23336
|
+
}
|
|
23337
|
+
if (!persisted) {
|
|
23338
|
+
log.dim(" (not a Genex project folder \u2014 the allowance is not saved here)");
|
|
23339
|
+
}
|
|
23340
|
+
if (snapshot) {
|
|
23341
|
+
const kinds = runnableKinds(snapshot.prices);
|
|
23342
|
+
if (kinds.length > 0) {
|
|
23343
|
+
log.plain("");
|
|
23344
|
+
log.plain(c.bold(" Prices (credits)"));
|
|
23345
|
+
for (const [kind, credits] of kinds) {
|
|
23346
|
+
log.plain(` ${kind.padEnd(20)} ${String(credits).padStart(5)}`);
|
|
23347
|
+
}
|
|
23348
|
+
}
|
|
23349
|
+
}
|
|
23350
|
+
log.plain("");
|
|
23351
|
+
log.dim(` ${BUDGET_SOURCE_LINE}`);
|
|
23352
|
+
if (enforced && !setting) {
|
|
23353
|
+
log.dim(` More for this game? Ask the player, then: ${c.cyan("npx genex budget --assets <credits> --user-approved")}`);
|
|
23354
|
+
}
|
|
23355
|
+
if (!snapshot && !setting) process.exitCode = 1;
|
|
23356
|
+
}
|
|
23357
|
+
|
|
23008
23358
|
// src/lib/costs.ts
|
|
23009
23359
|
var TYPICAL_CREDITS = {
|
|
23010
23360
|
model: 35,
|
|
@@ -23059,6 +23409,11 @@ ${c.bold("Usage")}
|
|
|
23059
23409
|
generation lanes are live. Exit 0 all clear,
|
|
23060
23410
|
1 if something needs fixing \u2014 run it first when
|
|
23061
23411
|
a command fails.
|
|
23412
|
+
genex budget [--json] This build's asset allowance: balance, allowance,
|
|
23413
|
+
spent so far, what's left, and live prices \u2014 plan
|
|
23414
|
+
the asset table against it. --assets <credits>
|
|
23415
|
+
--user-approved raises it, only once the player
|
|
23416
|
+
has agreed to the number.
|
|
23062
23417
|
genex auth [options] Connect this machine to your Genex account, or
|
|
23063
23418
|
finish a sign-in another command started \u2014 it
|
|
23064
23419
|
resumes the same code, so an interrupted
|
|
@@ -23551,6 +23906,9 @@ ${c.bold("Characters")}
|
|
|
23551
23906
|
${c.bold("Workflow")}
|
|
23552
23907
|
genex doctor [--json] Sign-in, credits, Node/CLI version, and which
|
|
23553
23908
|
lanes are live. Run it when anything fails.
|
|
23909
|
+
genex budget [--json] Balance, this build's asset allowance, spent,
|
|
23910
|
+
remaining, live prices. --assets <credits>
|
|
23911
|
+
--user-approved raises it (player's say-so first).
|
|
23554
23912
|
genex wait <id> Pick up a generation enqueued with --no-wait.
|
|
23555
23913
|
Never bills \u2014 re-running a generate command does.
|
|
23556
23914
|
genex wait --all One status line per generation from this folder.
|
|
@@ -23585,6 +23943,32 @@ Hosting, publishing, multiplayer, remixing and custom domains are the Genex
|
|
|
23585
23943
|
platform \u2014 build a game there from one prompt: ${c.cyan("https://genex.games")}
|
|
23586
23944
|
`;
|
|
23587
23945
|
var COMMAND_HELP = {
|
|
23946
|
+
budget: `${c.bold("genex budget")} \u2014 this build's asset allowance, and the live numbers to plan against.
|
|
23947
|
+
|
|
23948
|
+
${c.bold("Usage")}
|
|
23949
|
+
genex budget Balance \xB7 reserved \xB7 spendable; the allowance (default,
|
|
23950
|
+
or set by the player); spent this build; remaining;
|
|
23951
|
+
and the live price per kind, cheapest first.
|
|
23952
|
+
genex budget --json The same, as one object.
|
|
23953
|
+
genex budget --assets <credits> --user-approved
|
|
23954
|
+
Set the allowance to <credits> \u2014 a whole number, 0 or
|
|
23955
|
+
more. Refused without --user-approved: raising it needs
|
|
23956
|
+
the player's explicit approval of the number. 0 means
|
|
23957
|
+
no more paid generation in this build.
|
|
23958
|
+
|
|
23959
|
+
${c.bold("How the allowance works")}
|
|
23960
|
+
A per-build spend limit, checked before every paid generation in both workspace
|
|
23961
|
+
modes \u2014 the spend limit the shipping ratchet (preview between batches) is not.
|
|
23962
|
+
The default is derived ONCE, at the first paid command: ${DEFAULT_ASSET_BUDGET_SHARE === 0.5 ? "half" : `${Math.round(DEFAULT_ASSET_BUDGET_SHARE * 100)}%`} of the
|
|
23963
|
+
spendable balance, capped at ${DEFAULT_ASSET_BUDGET_CAP} credits. The wallet moving later does not move
|
|
23964
|
+
it \u2014 only --assets does. A refused generation names the numbers and the fix: ask
|
|
23965
|
+
the player, then re-run this with the number they agreed to; or build the thing in
|
|
23966
|
+
code and say why. Failed generations are refunded and never count.
|
|
23967
|
+
Stored in .genex/asset-budget.json. Operator knobs: GENEX_ASSET_BUDGET_SHARE (0-1)
|
|
23968
|
+
and GENEX_ASSET_BUDGET_CAP (credits; 0 turns the budget off).
|
|
23969
|
+
|
|
23970
|
+
Every number printed is live from your account; prices can change without a deploy.
|
|
23971
|
+
`,
|
|
23588
23972
|
remix: `genex remix <slug-or-Genex-page-URL> [directory] [--preview | --source-only] [--json]
|
|
23589
23973
|
|
|
23590
23974
|
Prepare source and assets at one pinned snapshot. The default destination is a fresh
|
|
@@ -23726,6 +24110,8 @@ function parseArgs(argv) {
|
|
|
23726
24110
|
"--approve-remesh",
|
|
23727
24111
|
"--category",
|
|
23728
24112
|
"--limit",
|
|
24113
|
+
// `genex budget --assets <credits>` — the allowance the player approved.
|
|
24114
|
+
"--assets",
|
|
23729
24115
|
// `genex motion` value flags.
|
|
23730
24116
|
"--takes",
|
|
23731
24117
|
"--seeds",
|
|
@@ -24158,6 +24544,14 @@ function applyValueFlag(options, flag, value) {
|
|
|
24158
24544
|
options.limit = n;
|
|
24159
24545
|
break;
|
|
24160
24546
|
}
|
|
24547
|
+
case "--assets": {
|
|
24548
|
+
const n = Number(value);
|
|
24549
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
24550
|
+
throw new Error(`Invalid --assets value: ${value} (whole credits, 0 or more)`);
|
|
24551
|
+
}
|
|
24552
|
+
options.assets = n;
|
|
24553
|
+
break;
|
|
24554
|
+
}
|
|
24161
24555
|
case "--timeout": {
|
|
24162
24556
|
const n = Number(value);
|
|
24163
24557
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -24436,6 +24830,12 @@ async function main() {
|
|
|
24436
24830
|
case "doctor":
|
|
24437
24831
|
await runDoctor(parsed.options);
|
|
24438
24832
|
break;
|
|
24833
|
+
// The asset allowance in the open (`lib/asset-budget.ts`): the numbers
|
|
24834
|
+
// an agent plans the asset table against, and the one door — with the
|
|
24835
|
+
// player's approval — through which the allowance moves.
|
|
24836
|
+
case "budget":
|
|
24837
|
+
await runBudget(parsed.options);
|
|
24838
|
+
break;
|
|
24439
24839
|
case "auth":
|
|
24440
24840
|
await runAuth(parsed.options);
|
|
24441
24841
|
break;
|