@genex-ai/cli-demo 1.9.3-dev.519 → 1.10.0-dev.521
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 +1069 -147
- package/package.json +1 -1
- package/templates/skills/genex-ai-image/SKILL.md +6 -1
- package/templates/skills/genex-tool-audio/SKILL.md +68 -0
- package/templates/skills/genex-tool-character/SKILL.md +94 -0
- package/templates/skills/genex-tool-image/SKILL.md +60 -0
- package/templates/skills/genex-tool-model/SKILL.md +63 -0
- package/templates/skills/genex-tool-texture/SKILL.md +54 -0
- package/templates/skills/genex-tool-video/SKILL.md +59 -0
- package/templates/skills/genex-tool-workflow/SKILL.md +95 -0
package/dist/index.js
CHANGED
|
@@ -219,6 +219,11 @@ var c = {
|
|
|
219
219
|
|
|
220
220
|
// src/lib/api.ts
|
|
221
221
|
var CLI_VERSION_HEADER = "x-genex-cli-version";
|
|
222
|
+
var WORKSPACE_HEADER = "x-genex-workspace";
|
|
223
|
+
var workspaceLabel = null;
|
|
224
|
+
function setWorkspaceHeader(label) {
|
|
225
|
+
workspaceLabel = label;
|
|
226
|
+
}
|
|
222
227
|
function formatUpdateRequired(body) {
|
|
223
228
|
const action = body.action ?? `npm i -D @genex-ai/cli-demo@${CLI_CHANNEL}`;
|
|
224
229
|
const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
|
|
@@ -255,6 +260,9 @@ function printedStructuredError(res) {
|
|
|
255
260
|
async function apiFetch(url, init2 = {}) {
|
|
256
261
|
const headers = new Headers(init2.headers);
|
|
257
262
|
if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
|
|
263
|
+
if (workspaceLabel && !headers.has(WORKSPACE_HEADER)) {
|
|
264
|
+
headers.set(WORKSPACE_HEADER, workspaceLabel);
|
|
265
|
+
}
|
|
258
266
|
const res = await fetch(url, { ...init2, headers });
|
|
259
267
|
if (res.status === 426) {
|
|
260
268
|
try {
|
|
@@ -816,6 +824,25 @@ import path4 from "path";
|
|
|
816
824
|
function getProjectMetadataPath(cwd = process.cwd()) {
|
|
817
825
|
return path4.join(cwd, ".genex", "project.json");
|
|
818
826
|
}
|
|
827
|
+
function getWorkspacePath(cwd = process.cwd()) {
|
|
828
|
+
return path4.join(cwd, ".genex", "workspace.json");
|
|
829
|
+
}
|
|
830
|
+
async function readWorkspace(cwd = process.cwd()) {
|
|
831
|
+
try {
|
|
832
|
+
const raw = await fs4.readFile(getWorkspacePath(cwd), "utf8");
|
|
833
|
+
return JSON.parse(raw);
|
|
834
|
+
} catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function writeWorkspace(meta, cwd = process.cwd()) {
|
|
839
|
+
const file = getWorkspacePath(cwd);
|
|
840
|
+
await fs4.mkdir(path4.dirname(file), { recursive: true });
|
|
841
|
+
await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
|
|
842
|
+
await fs4.chmod(file, 384).catch(() => {
|
|
843
|
+
});
|
|
844
|
+
return { path: file };
|
|
845
|
+
}
|
|
819
846
|
async function writeUserToken(token, envPath) {
|
|
820
847
|
const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
|
|
821
848
|
return { path: written };
|
|
@@ -954,6 +981,7 @@ async function walk(rootSrc, src, dest, opts, result) {
|
|
|
954
981
|
const destPath = path5.join(dest, entry.name);
|
|
955
982
|
const rel = path5.relative(rootSrc, srcPath);
|
|
956
983
|
if (opts.exclude?.includes(rel)) continue;
|
|
984
|
+
if (opts.filter && !opts.filter(rel)) continue;
|
|
957
985
|
if (entry.isDirectory()) {
|
|
958
986
|
await fs5.mkdir(destPath, { recursive: true });
|
|
959
987
|
await walk(rootSrc, srcPath, destPath, opts, result);
|
|
@@ -1017,10 +1045,20 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
1017
1045
|
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.
|
|
1018
1046
|
${CONTRACT_END}
|
|
1019
1047
|
`;
|
|
1048
|
+
var GENEX_TOOLS_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
1049
|
+
# Genex Tools (always in effect in this folder)
|
|
1050
|
+
|
|
1051
|
+
This folder uses **Genex Tools**: AI asset generation for games, from your terminal. All via \`npx genex \u2026\` \u2014 \`model "<prompt>"\` (plus \`model --image <file>\` from a reference photo, \`model segment <id>\` into named parts, \`model rig <id>\` across 7 body plans, \`model animate <rig-id> --preset walk\`) \xB7 \`image\` (\`--transparent\` \xB7 \`--edit\` \xB7 \`--inpaint\` \xB7 \`--remove-bg\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 \`texture\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 rigged \`character\` / \`creature\` and \`character animate <id> "<verb>"\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`doctor\`. Full options with costs: \`npx genex --help\`. Each tool has a skill card (\`genex-tool-\u2026\`) \u2014 load the one for the lane you are using. Whatever this project builds and wherever it ships is the user's own; Genex generates the assets and nothing more.
|
|
1052
|
+
|
|
1053
|
+
1. **NEVER delete, empty, move, rename, or overwrite anything you did not create yourself.** This is someone's real project: it holds their code, their reference images, their notes, an earlier attempt \u2014 files that may 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 prompt, and every tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files"). 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.
|
|
1054
|
+
2. **Generated assets are local files the user owns.** Every generation downloads into \`./assets/\` and the command prints the local path \u2014 wire THAT path into the code, exactly like an asset they drew themselves. The URL printed beside it is provenance, not hosting: never fetch it at runtime, never ship a build that depends on it. Assets are theirs to move, rename, commit, or process further.
|
|
1055
|
+
3. **Run \`npx genex doctor\` when a command fails, and before promising what you can generate.** It reports sign-in, credit balance, and which generation lanes are actually live on this account \u2014 the only live source for all three. Treat every genex warning line as work, not noise, and never re-run a generation command to "retry" one that is still running: that bills a second asset. Attach to the first with \`npx genex wait <id>\`.
|
|
1056
|
+
${CONTRACT_END}
|
|
1057
|
+
`;
|
|
1020
1058
|
var CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
1021
1059
|
var CONTRACT_REGION = /<!-- genex:contract:begin[^\n]*-->[\s\S]*?<!-- genex:contract:end -->/;
|
|
1022
|
-
function mergeContractBlock(existing) {
|
|
1023
|
-
const block =
|
|
1060
|
+
function mergeContractBlock(existing, contractBlock = GENEX_CONTRACT_BLOCK) {
|
|
1061
|
+
const block = contractBlock.trimEnd();
|
|
1024
1062
|
if (existing === null || existing.trim() === "") return `${block}
|
|
1025
1063
|
`;
|
|
1026
1064
|
if (CONTRACT_REGION.test(existing)) {
|
|
@@ -1035,7 +1073,7 @@ function mergeContractBlock(existing) {
|
|
|
1035
1073
|
${block}
|
|
1036
1074
|
`;
|
|
1037
1075
|
}
|
|
1038
|
-
async function writeAgentsContract(projectDir) {
|
|
1076
|
+
async function writeAgentsContract(projectDir, contractBlock = GENEX_CONTRACT_BLOCK) {
|
|
1039
1077
|
let changed = false;
|
|
1040
1078
|
try {
|
|
1041
1079
|
const agentsPath = path6.join(projectDir, "AGENTS.md");
|
|
@@ -1045,7 +1083,7 @@ async function writeAgentsContract(projectDir) {
|
|
|
1045
1083
|
} catch {
|
|
1046
1084
|
existing = null;
|
|
1047
1085
|
}
|
|
1048
|
-
const next = mergeContractBlock(existing);
|
|
1086
|
+
const next = mergeContractBlock(existing, contractBlock);
|
|
1049
1087
|
if (next !== existing) {
|
|
1050
1088
|
await fs6.writeFile(agentsPath, next, "utf8");
|
|
1051
1089
|
changed = true;
|
|
@@ -1072,19 +1110,25 @@ ${CLAUDE_IMPORT_LINE}
|
|
|
1072
1110
|
}
|
|
1073
1111
|
return changed;
|
|
1074
1112
|
}
|
|
1113
|
+
async function writeToolsContract(projectDir) {
|
|
1114
|
+
return writeAgentsContract(projectDir, GENEX_TOOLS_CONTRACT_BLOCK);
|
|
1115
|
+
}
|
|
1075
1116
|
async function healAncestorContracts(projectDir, stopDir = os4.homedir()) {
|
|
1076
1117
|
const findings = [];
|
|
1077
1118
|
const stop = path6.resolve(stopDir);
|
|
1078
1119
|
let dir = path6.dirname(path6.resolve(projectDir));
|
|
1079
1120
|
for (let depth = 0; depth < 20; depth++) {
|
|
1080
1121
|
try {
|
|
1081
|
-
let
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1122
|
+
let isWorkspace = false;
|
|
1123
|
+
for (const marker of ["project.json", "workspace.json"]) {
|
|
1124
|
+
try {
|
|
1125
|
+
await fs6.access(path6.join(dir, ".genex", marker));
|
|
1126
|
+
isWorkspace = true;
|
|
1127
|
+
break;
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1086
1130
|
}
|
|
1087
|
-
if (!
|
|
1131
|
+
if (!isWorkspace) {
|
|
1088
1132
|
const agentsPath = path6.join(dir, "AGENTS.md");
|
|
1089
1133
|
let content = null;
|
|
1090
1134
|
try {
|
|
@@ -1142,6 +1186,45 @@ async function healAncestorContractsAndReport(log, projectDir) {
|
|
|
1142
1186
|
import fs7 from "fs/promises";
|
|
1143
1187
|
import os5 from "os";
|
|
1144
1188
|
import path7 from "path";
|
|
1189
|
+
|
|
1190
|
+
// src/lib/workspace.ts
|
|
1191
|
+
async function workspaceMode(cwd = process.cwd()) {
|
|
1192
|
+
if (await readProject(cwd)) return "game";
|
|
1193
|
+
const ws = await readWorkspace(cwd);
|
|
1194
|
+
return ws?.mode === "tools" ? "tools" : null;
|
|
1195
|
+
}
|
|
1196
|
+
var REFUSED_IN_TOOLS = /* @__PURE__ */ new Set([
|
|
1197
|
+
"link",
|
|
1198
|
+
"pull",
|
|
1199
|
+
"list",
|
|
1200
|
+
"preview",
|
|
1201
|
+
"promote",
|
|
1202
|
+
"publish",
|
|
1203
|
+
"rollback",
|
|
1204
|
+
"rename",
|
|
1205
|
+
"make-remixable",
|
|
1206
|
+
"shop",
|
|
1207
|
+
"domain",
|
|
1208
|
+
"asset",
|
|
1209
|
+
"controller"
|
|
1210
|
+
]);
|
|
1211
|
+
function refusedInTools(command) {
|
|
1212
|
+
return command !== void 0 && REFUSED_IN_TOOLS.has(command);
|
|
1213
|
+
}
|
|
1214
|
+
function reportPlatformRefused(log, command) {
|
|
1215
|
+
log.error(`\`genex ${command}\` is not part of Genex Tools.`);
|
|
1216
|
+
log.plain(
|
|
1217
|
+
[
|
|
1218
|
+
" This workspace is the asset toolkit: generation only. Hosting, publishing, multiplayer,",
|
|
1219
|
+
" remixing and custom domains are the Genex platform \u2014 a game built there gets all of it",
|
|
1220
|
+
` from one prompt: ${c.cyan("https://genex.games")}`,
|
|
1221
|
+
` ${c.cyan("\u2192")} Everything here still works: ${c.cyan("npx genex model|image|video|texture|sfx|music|voice|character")}.`,
|
|
1222
|
+
` ${c.cyan("\u2192")} ${c.cyan("npx genex doctor")} lists what this account can generate right now.`
|
|
1223
|
+
].join("\n")
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/lib/updates.ts
|
|
1145
1228
|
function parseSemver(v) {
|
|
1146
1229
|
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
1147
1230
|
if (!m) return null;
|
|
@@ -1181,6 +1264,22 @@ async function hasGenexSkills(skillsDir) {
|
|
|
1181
1264
|
return false;
|
|
1182
1265
|
}
|
|
1183
1266
|
}
|
|
1267
|
+
var TOOL_SKILL_PREFIX = "genex-tool-";
|
|
1268
|
+
function skillFamilyOf(name) {
|
|
1269
|
+
return name.startsWith(TOOL_SKILL_PREFIX) ? "tools" : "game";
|
|
1270
|
+
}
|
|
1271
|
+
function skillFamilyFilter(mode) {
|
|
1272
|
+
return (rel) => {
|
|
1273
|
+
const segments = rel.split(/[\\/]/);
|
|
1274
|
+
const top = segments[0] ?? "";
|
|
1275
|
+
if (top !== "skills") {
|
|
1276
|
+
return mode === "game";
|
|
1277
|
+
}
|
|
1278
|
+
const skill = segments[1];
|
|
1279
|
+
if (skill === void 0) return true;
|
|
1280
|
+
return skillFamilyOf(skill) === mode;
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1184
1283
|
var REMOVED_SKILLS = [
|
|
1185
1284
|
"genex-explore",
|
|
1186
1285
|
"genex-threejs-procedural-materials",
|
|
@@ -1232,13 +1331,18 @@ async function pruneRemovedSkills(skillsDir, log) {
|
|
|
1232
1331
|
}
|
|
1233
1332
|
return removed.length > 0;
|
|
1234
1333
|
}
|
|
1235
|
-
async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log) {
|
|
1334
|
+
async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log, family = "game") {
|
|
1236
1335
|
const skillsDir = path7.join(target.baseDir, "skills");
|
|
1237
1336
|
if (!await hasGenexSkills(skillsDir)) return false;
|
|
1238
1337
|
if (await readSkillsMarker(skillsDir) === version) return false;
|
|
1239
1338
|
const src = target.full ? templatesDir : path7.join(templatesDir, "skills");
|
|
1240
1339
|
const dest = target.full ? target.baseDir : skillsDir;
|
|
1241
|
-
|
|
1340
|
+
const familyFilter = skillFamilyFilter(family);
|
|
1341
|
+
const filter = target.full ? familyFilter : (rel) => familyFilter(`skills/${rel}`);
|
|
1342
|
+
await copyTemplates(src, dest, {
|
|
1343
|
+
exclude: ["controllers", "motion", "asset-viewer"],
|
|
1344
|
+
filter
|
|
1345
|
+
});
|
|
1242
1346
|
await pruneRemovedSkills(skillsDir, log);
|
|
1243
1347
|
await writeSkillsMarker(skillsDir, version);
|
|
1244
1348
|
return true;
|
|
@@ -1287,23 +1391,32 @@ async function cleanupLegacyGlobalSkills(log) {
|
|
|
1287
1391
|
return false;
|
|
1288
1392
|
}
|
|
1289
1393
|
}
|
|
1290
|
-
async function syncSkills(log) {
|
|
1394
|
+
async function syncSkills(log, mode) {
|
|
1291
1395
|
try {
|
|
1292
1396
|
await cleanupLegacyGlobalSkills(log);
|
|
1293
1397
|
const version = getCliVersion();
|
|
1294
1398
|
const templatesDir = getTemplatesDir();
|
|
1399
|
+
const resolved = mode !== void 0 ? mode : await workspaceMode();
|
|
1400
|
+
const family = resolved === "tools" ? "tools" : "game";
|
|
1295
1401
|
let refreshed = false;
|
|
1296
1402
|
for (const target of resolveAgentTargets()) {
|
|
1297
1403
|
try {
|
|
1298
|
-
refreshed = await syncSkillsForTarget(target, templatesDir, version, log) || refreshed;
|
|
1404
|
+
refreshed = await syncSkillsForTarget(target, templatesDir, version, log, family) || refreshed;
|
|
1299
1405
|
} catch {
|
|
1300
1406
|
}
|
|
1301
1407
|
}
|
|
1302
1408
|
if (refreshed) log.plain(`\u{1F504} Genex skills updated to ${version}`);
|
|
1303
1409
|
try {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1410
|
+
if (resolved === "game") {
|
|
1411
|
+
if (await writeAgentsContract(process.cwd())) {
|
|
1412
|
+
log.plain("\u{1F504} Genex build contract refreshed (AGENTS.md managed block)");
|
|
1413
|
+
}
|
|
1414
|
+
} else if (resolved === "tools") {
|
|
1415
|
+
if (await writeToolsContract(process.cwd())) {
|
|
1416
|
+
log.plain("\u{1F504} Genex Tools workspace rules refreshed (AGENTS.md managed block)");
|
|
1417
|
+
}
|
|
1418
|
+
} else {
|
|
1419
|
+
return;
|
|
1307
1420
|
}
|
|
1308
1421
|
await healAncestorContractsAndReport(log, process.cwd());
|
|
1309
1422
|
} catch {
|
|
@@ -1706,6 +1819,19 @@ function warnPreexisting(log, entries) {
|
|
|
1706
1819
|
}
|
|
1707
1820
|
async function runInit(opts) {
|
|
1708
1821
|
const log = createLogger({ quiet: opts.quiet });
|
|
1822
|
+
if ((await readWorkspace())?.mode === "tools") {
|
|
1823
|
+
log.error("This folder is a Genex Tools workspace.");
|
|
1824
|
+
log.plain(
|
|
1825
|
+
[
|
|
1826
|
+
" It has the generation toolkit installed for a game you host yourself \u2014 creating a",
|
|
1827
|
+
" hosted genex game here would install a second, conflicting set of agent instructions.",
|
|
1828
|
+
` ${c.cyan("\u2192")} Building a genex-hosted game? Run ${c.cyan("npx genex init")} in its own folder.`,
|
|
1829
|
+
` ${c.cyan("\u2192")} Just want the tools working? ${c.cyan("npx genex doctor")} says what needs fixing.`
|
|
1830
|
+
].join("\n")
|
|
1831
|
+
);
|
|
1832
|
+
process.exitCode = 1;
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1709
1835
|
log.plain(c.bold("genex init"));
|
|
1710
1836
|
log.plain("");
|
|
1711
1837
|
const preexisting = await listPreexistingEntries(process.cwd());
|
|
@@ -1722,7 +1848,8 @@ async function runInit(opts) {
|
|
|
1722
1848
|
const dest = t.full ? t.baseDir : path10.join(t.baseDir, "skills");
|
|
1723
1849
|
const { copied, updated } = await copyTemplates(src, dest, {
|
|
1724
1850
|
force: opts.force,
|
|
1725
|
-
exclude: ["controllers", "motion", "asset-viewer"]
|
|
1851
|
+
exclude: ["controllers", "motion", "asset-viewer"],
|
|
1852
|
+
filter: t.full ? skillFamilyFilter("game") : (rel) => skillFamilyFilter("game")(`skills/${rel}`)
|
|
1726
1853
|
});
|
|
1727
1854
|
await pruneRemovedSkills(path10.join(t.baseDir, "skills"), log);
|
|
1728
1855
|
await writeSkillsMarker(path10.join(t.baseDir, "skills"));
|
|
@@ -6685,8 +6812,8 @@ async function runRollback(opts) {
|
|
|
6685
6812
|
}
|
|
6686
6813
|
|
|
6687
6814
|
// src/commands/generate.ts
|
|
6688
|
-
import
|
|
6689
|
-
import
|
|
6815
|
+
import fs21 from "fs/promises";
|
|
6816
|
+
import path20 from "path";
|
|
6690
6817
|
import { PNG as PNG4 } from "pngjs";
|
|
6691
6818
|
|
|
6692
6819
|
// src/lib/glass.ts
|
|
@@ -6766,6 +6893,96 @@ function solveMagentaGlass(source) {
|
|
|
6766
6893
|
return { ...base, png: out, ok: true };
|
|
6767
6894
|
}
|
|
6768
6895
|
|
|
6896
|
+
// src/lib/download-assets.ts
|
|
6897
|
+
import fs20 from "fs/promises";
|
|
6898
|
+
import path19 from "path";
|
|
6899
|
+
var RUNG_ROLE = /@\d+$/;
|
|
6900
|
+
function isPrimaryRole(role) {
|
|
6901
|
+
return !RUNG_ROLE.test(role);
|
|
6902
|
+
}
|
|
6903
|
+
function primaryFiles(files) {
|
|
6904
|
+
return files.filter((f) => isPrimaryRole(f.role));
|
|
6905
|
+
}
|
|
6906
|
+
function localAssetName(kind, prompt, id, file, multi) {
|
|
6907
|
+
const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
6908
|
+
const base = `${slug || kind}-${id.slice(0, 8)}`;
|
|
6909
|
+
const role = multi ? `-${file.role.replace(/^(texture|image|character|model|model_anim|model-anim)-/, "")}` : "";
|
|
6910
|
+
const ext = file.ext.replace(/^\./, "") || "bin";
|
|
6911
|
+
return `${base}${role}.${ext}`;
|
|
6912
|
+
}
|
|
6913
|
+
async function downloadAssets(files, opts) {
|
|
6914
|
+
const wanted = primaryFiles(files);
|
|
6915
|
+
const result = { saved: [], failures: [] };
|
|
6916
|
+
if (wanted.length === 0) return result;
|
|
6917
|
+
try {
|
|
6918
|
+
await fs20.mkdir(opts.outDir, { recursive: true });
|
|
6919
|
+
} catch (err) {
|
|
6920
|
+
result.failures.push(
|
|
6921
|
+
`couldn't create ${opts.outDir} (${err instanceof Error ? err.message : String(err)})`
|
|
6922
|
+
);
|
|
6923
|
+
return result;
|
|
6924
|
+
}
|
|
6925
|
+
const multi = wanted.length > 1;
|
|
6926
|
+
for (const file of wanted) {
|
|
6927
|
+
const dest = path19.join(opts.outDir, localAssetName(opts.kind, opts.prompt, opts.id, file, multi));
|
|
6928
|
+
try {
|
|
6929
|
+
const res = await fetch(file.url);
|
|
6930
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6931
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
6932
|
+
await fs20.writeFile(dest, buf);
|
|
6933
|
+
result.saved.push({ role: file.role, path: dest, url: file.url, bytes: buf.byteLength });
|
|
6934
|
+
} catch (err) {
|
|
6935
|
+
result.failures.push(
|
|
6936
|
+
`${file.role}: ${err instanceof Error ? err.message : String(err)} \u2014 the asset is still live at ${file.url}`
|
|
6937
|
+
);
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6940
|
+
return result;
|
|
6941
|
+
}
|
|
6942
|
+
function formatBytes(bytes) {
|
|
6943
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6944
|
+
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
6945
|
+
return `${bytes} B`;
|
|
6946
|
+
}
|
|
6947
|
+
var DEFAULT_ASSET_DIR = "./assets";
|
|
6948
|
+
function localDeliveryFor(mode, opts, prompt) {
|
|
6949
|
+
if (mode !== "tools" || opts.noDownload) return void 0;
|
|
6950
|
+
return { outDir: opts.outDir ?? DEFAULT_ASSET_DIR, prompt };
|
|
6951
|
+
}
|
|
6952
|
+
|
|
6953
|
+
// src/lib/lanes.ts
|
|
6954
|
+
async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
|
|
6955
|
+
try {
|
|
6956
|
+
const res = await apiFetch(`${apiUrl}/api/generations/lanes`, {
|
|
6957
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
6958
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
6959
|
+
});
|
|
6960
|
+
if (!res.ok) return null;
|
|
6961
|
+
const body = await res.json();
|
|
6962
|
+
if (!Array.isArray(body.lanes)) return null;
|
|
6963
|
+
return {
|
|
6964
|
+
lanes: body.lanes.filter(
|
|
6965
|
+
(l) => !!l && typeof l.kind === "string" && typeof l.provider === "string"
|
|
6966
|
+
),
|
|
6967
|
+
paused: body.paused === true
|
|
6968
|
+
};
|
|
6969
|
+
} catch {
|
|
6970
|
+
return null;
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
function laneFor(report, kind) {
|
|
6974
|
+
return report?.lanes.find((l) => l.kind === kind);
|
|
6975
|
+
}
|
|
6976
|
+
function mockLaneRefusal(kind, provider) {
|
|
6977
|
+
return [
|
|
6978
|
+
`The \`${kind}\` lane on this server has no provider key \u2014 it is running in SAMPLE mode (${provider}).`,
|
|
6979
|
+
" A generation would succeed, spend credits, and return a placeholder asset that looks real.",
|
|
6980
|
+
" Refusing before anything is charged.",
|
|
6981
|
+
" \u2192 On genex.games every lane is live; this is a self-hosted or partially configured stand.",
|
|
6982
|
+
" \u2192 `npx genex doctor` lists every lane's status."
|
|
6983
|
+
];
|
|
6984
|
+
}
|
|
6985
|
+
|
|
6769
6986
|
// src/lib/open.ts
|
|
6770
6987
|
import { spawn as spawn5 } from "child_process";
|
|
6771
6988
|
function tokenize(cmd) {
|
|
@@ -6856,7 +7073,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
6856
7073
|
async function inlineLocalImage(filePath, flag) {
|
|
6857
7074
|
let bytes;
|
|
6858
7075
|
try {
|
|
6859
|
-
bytes = await
|
|
7076
|
+
bytes = await fs21.readFile(filePath);
|
|
6860
7077
|
} catch {
|
|
6861
7078
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
6862
7079
|
}
|
|
@@ -6866,7 +7083,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
6866
7083
|
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.`
|
|
6867
7084
|
};
|
|
6868
7085
|
}
|
|
6869
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
7086
|
+
const mime = IMAGE_MIME_BY_EXT[path20.extname(filePath).toLowerCase()] ?? "image/png";
|
|
6870
7087
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
6871
7088
|
}
|
|
6872
7089
|
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.";
|
|
@@ -7023,7 +7240,7 @@ async function runGenerate(kind, opts) {
|
|
|
7023
7240
|
let typedPrompt = opts.prompt?.trim();
|
|
7024
7241
|
if (!typedPrompt && kind === "model" && opts.imageUrl) {
|
|
7025
7242
|
const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
|
|
7026
|
-
typedPrompt = `from image: ${
|
|
7243
|
+
typedPrompt = `from image: ${path20.basename(ref).slice(0, 120)}`;
|
|
7027
7244
|
}
|
|
7028
7245
|
if (!typedPrompt) {
|
|
7029
7246
|
log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
|
|
@@ -7067,7 +7284,7 @@ async function runGenerate(kind, opts) {
|
|
|
7067
7284
|
return;
|
|
7068
7285
|
}
|
|
7069
7286
|
try {
|
|
7070
|
-
const bytes = await
|
|
7287
|
+
const bytes = await fs21.readFile(opts.inpaintUrl);
|
|
7071
7288
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
7072
7289
|
} catch {
|
|
7073
7290
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -7115,6 +7332,17 @@ async function runGenerate(kind, opts) {
|
|
|
7115
7332
|
}
|
|
7116
7333
|
const meta = await readProject();
|
|
7117
7334
|
const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
|
|
7335
|
+
const mode = await workspaceMode();
|
|
7336
|
+
if (mode === "tools") {
|
|
7337
|
+
const lane = laneFor(await fetchLanes(apiUrl, token), kind);
|
|
7338
|
+
if (lane?.mock) {
|
|
7339
|
+
const [headline, ...rest] = mockLaneRefusal(kind, lane.provider);
|
|
7340
|
+
log.error(headline);
|
|
7341
|
+
log.plain(rest.join("\n"));
|
|
7342
|
+
process.exitCode = 1;
|
|
7343
|
+
return;
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7118
7346
|
const options = buildGenOptions(kind, opts);
|
|
7119
7347
|
log.plain(c.bold(`genex ${kind}`));
|
|
7120
7348
|
log.dim(` ${prompt}`);
|
|
@@ -7187,10 +7415,20 @@ async function runGenerate(kind, opts) {
|
|
|
7187
7415
|
process.exitCode = 1;
|
|
7188
7416
|
return id;
|
|
7189
7417
|
}
|
|
7190
|
-
|
|
7418
|
+
const glassDir = opts.outDir ?? (mode === "tools" ? DEFAULT_ASSET_DIR : ".");
|
|
7419
|
+
await reportGlassTerminal(view, glassDir, log, opts.json ?? false);
|
|
7191
7420
|
return id;
|
|
7192
7421
|
}
|
|
7193
|
-
await awaitAndReport(
|
|
7422
|
+
await awaitAndReport(
|
|
7423
|
+
apiUrl,
|
|
7424
|
+
token,
|
|
7425
|
+
id,
|
|
7426
|
+
kind,
|
|
7427
|
+
log,
|
|
7428
|
+
opts.open,
|
|
7429
|
+
opts.json,
|
|
7430
|
+
localDeliveryFor(mode, opts, prompt)
|
|
7431
|
+
);
|
|
7194
7432
|
return id;
|
|
7195
7433
|
}
|
|
7196
7434
|
async function reportGlassTerminal(view, outDir, log, json) {
|
|
@@ -7200,7 +7438,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
7200
7438
|
return;
|
|
7201
7439
|
}
|
|
7202
7440
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
7203
|
-
await
|
|
7441
|
+
await fs21.mkdir(outDir, { recursive: true });
|
|
7204
7442
|
const solved = [];
|
|
7205
7443
|
for (let i = 0; i < files.length; i++) {
|
|
7206
7444
|
const f = files[i];
|
|
@@ -7232,8 +7470,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
7232
7470
|
});
|
|
7233
7471
|
continue;
|
|
7234
7472
|
}
|
|
7235
|
-
const outPath =
|
|
7236
|
-
await
|
|
7473
|
+
const outPath = path20.join(outDir, `glass-${i + 1}.png`);
|
|
7474
|
+
await fs21.writeFile(outPath, PNG4.sync.write(r.png));
|
|
7237
7475
|
solved.push({
|
|
7238
7476
|
path: outPath,
|
|
7239
7477
|
url: f.url,
|
|
@@ -7269,7 +7507,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
7269
7507
|
process.exitCode = 1;
|
|
7270
7508
|
}
|
|
7271
7509
|
}
|
|
7272
|
-
async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false) {
|
|
7510
|
+
async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false, local) {
|
|
7273
7511
|
if (!json) log.step("Generating\u2026 (this can take up to a minute)");
|
|
7274
7512
|
const view = await waitForTerminalView(
|
|
7275
7513
|
apiUrl,
|
|
@@ -7285,7 +7523,7 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json =
|
|
|
7285
7523
|
process.exitCode = 1;
|
|
7286
7524
|
return;
|
|
7287
7525
|
}
|
|
7288
|
-
await reportTerminal(kind, view, log, open, json);
|
|
7526
|
+
await reportTerminal(kind, view, log, open, json, local);
|
|
7289
7527
|
}
|
|
7290
7528
|
async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
|
|
7291
7529
|
}) {
|
|
@@ -7294,7 +7532,7 @@ async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
|
|
|
7294
7532
|
const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
|
|
7295
7533
|
return streamed === "unsupported" ? poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
|
|
7296
7534
|
}
|
|
7297
|
-
async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
7535
|
+
async function reportTerminal(kind, view, log, open = false, json = false, local) {
|
|
7298
7536
|
if (view.status !== "completed") {
|
|
7299
7537
|
if (json) writeJson({ kind, id: view.id, status: view.status, error: view.error ?? null });
|
|
7300
7538
|
else log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
@@ -7319,12 +7557,20 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
|
7319
7557
|
return;
|
|
7320
7558
|
}
|
|
7321
7559
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
7560
|
+
const delivered = local ? await downloadAssets(files, {
|
|
7561
|
+
kind,
|
|
7562
|
+
prompt: local.prompt,
|
|
7563
|
+
id: view.id,
|
|
7564
|
+
outDir: local.outDir
|
|
7565
|
+
}) : null;
|
|
7322
7566
|
if (kind === "character_concept") {
|
|
7323
7567
|
reportCharacterConceptTerminal(view, log, { json, open });
|
|
7568
|
+
if (delivered) reportLocalFiles(delivered, log, json);
|
|
7324
7569
|
return;
|
|
7325
7570
|
}
|
|
7326
7571
|
if (kind === "character_preview") {
|
|
7327
7572
|
reportCharacterPreviewTerminal(view, log, { json, open });
|
|
7573
|
+
if (delivered) reportLocalFiles(delivered, log, json);
|
|
7328
7574
|
return;
|
|
7329
7575
|
}
|
|
7330
7576
|
if (json) {
|
|
@@ -7333,6 +7579,7 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
|
7333
7579
|
id: view.id,
|
|
7334
7580
|
status: view.status,
|
|
7335
7581
|
files,
|
|
7582
|
+
...delivered ? { saved: delivered.saved, downloadFailures: delivered.failures } : {},
|
|
7336
7583
|
...kind === "character" ? {
|
|
7337
7584
|
characterId: view.id,
|
|
7338
7585
|
nextCommand: `genex controller character --character ${view.id}`
|
|
@@ -7341,10 +7588,14 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
|
7341
7588
|
return;
|
|
7342
7589
|
}
|
|
7343
7590
|
log.plain("");
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
log.
|
|
7591
|
+
if (delivered) {
|
|
7592
|
+
reportLocalFiles(delivered, log, false);
|
|
7593
|
+
} else {
|
|
7594
|
+
log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
|
|
7595
|
+
for (const f of files) {
|
|
7596
|
+
const label = files.length > 1 ? `${c.dim(`${f.role.replace(/^texture-/, "")}: `)}` : "";
|
|
7597
|
+
log.plain(` ${label}${f.url}`);
|
|
7598
|
+
}
|
|
7348
7599
|
}
|
|
7349
7600
|
log.plain("");
|
|
7350
7601
|
if (kind === "texture" && files[0]) await reportTextureSeam(files[0].url, log);
|
|
@@ -7362,7 +7613,22 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
|
|
|
7362
7613
|
log.plain(` ${files[0].url}`);
|
|
7363
7614
|
log.plain("");
|
|
7364
7615
|
}
|
|
7365
|
-
printHint(kind, view, files, log);
|
|
7616
|
+
printHint(kind, view, files, log, delivered !== null);
|
|
7617
|
+
}
|
|
7618
|
+
function reportLocalFiles(result, log, json) {
|
|
7619
|
+
if (json) return;
|
|
7620
|
+
if (result.saved.length > 0) {
|
|
7621
|
+
log.success(
|
|
7622
|
+
`Done \u2014 ${result.saved.length} file${result.saved.length === 1 ? "" : "s"} saved to your project:`
|
|
7623
|
+
);
|
|
7624
|
+
for (const s of result.saved) {
|
|
7625
|
+
log.plain(` ${s.path} ${c.dim(`(${formatBytes(s.bytes)})`)}`);
|
|
7626
|
+
}
|
|
7627
|
+
log.dim(` source of record (provenance, not hosting): ${result.saved[0].url}`);
|
|
7628
|
+
}
|
|
7629
|
+
for (const failure of result.failures) {
|
|
7630
|
+
log.error(`Couldn't save one file \u2014 ${failure}`);
|
|
7631
|
+
}
|
|
7366
7632
|
}
|
|
7367
7633
|
function laneFailureAdvice(kind, outcomes) {
|
|
7368
7634
|
if (outcomes.failed < 2 || outcomes.completed > 0) return null;
|
|
@@ -7529,9 +7795,11 @@ function assetHint(kind, view, url) {
|
|
|
7529
7795
|
const base = hint[kind];
|
|
7530
7796
|
return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
|
|
7531
7797
|
}
|
|
7532
|
-
function printHint(kind, view, files, log) {
|
|
7533
|
-
log.dim(` ${assetHint(kind, view, files[0]?.url ?? "")}`);
|
|
7534
|
-
log.dim(
|
|
7798
|
+
function printHint(kind, view, files, log, local = false) {
|
|
7799
|
+
log.dim(` ${assetHint(kind, view, local ? void 0 : files[0]?.url ?? "")}`);
|
|
7800
|
+
log.dim(
|
|
7801
|
+
local ? " Wire the LOCAL path into your code \u2014 the file is yours. Never fetch the URL at runtime." : " Reference the URL directly in your code \u2014 don't download it into the repo."
|
|
7802
|
+
);
|
|
7535
7803
|
}
|
|
7536
7804
|
var CONCEPT_STOP = "STOP \u2014 show all three concept images to the user and wait for an explicit selection. Do not create the 3D preview until they choose candidate 1, 2, or 3.";
|
|
7537
7805
|
function metadataOf(view) {
|
|
@@ -7833,11 +8101,13 @@ async function runWait(opts) {
|
|
|
7833
8101
|
return;
|
|
7834
8102
|
}
|
|
7835
8103
|
const kind = view.kind ?? "image";
|
|
8104
|
+
const ledgerPrompt = (await readLedger(opts.cwd ?? process.cwd())).find((e) => e.id === id)?.prompt;
|
|
8105
|
+
const local = localDeliveryFor(await workspaceMode(), opts, ledgerPrompt || kind);
|
|
7836
8106
|
log.plain(c.bold("genex wait"));
|
|
7837
8107
|
log.dim(` ${kind} ${id}`);
|
|
7838
8108
|
log.plain("");
|
|
7839
8109
|
if (TERMINAL2.has(view.status)) {
|
|
7840
|
-
await reportTerminal(kind, view, log, opts.open, opts.json);
|
|
8110
|
+
await reportTerminal(kind, view, log, opts.open, opts.json, local);
|
|
7841
8111
|
return;
|
|
7842
8112
|
}
|
|
7843
8113
|
if (kind === "character_concept" || kind === "character_preview" || kind === "character") {
|
|
@@ -7845,7 +8115,7 @@ async function runWait(opts) {
|
|
|
7845
8115
|
` \u23F3 Character stages take several minutes server-side \u2014 you don't have to sit here. Keep building and check back with ${c.cyan("genex wait --all")}.`
|
|
7846
8116
|
);
|
|
7847
8117
|
}
|
|
7848
|
-
await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
|
|
8118
|
+
await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json, local);
|
|
7849
8119
|
}
|
|
7850
8120
|
async function runWaitAll(opts) {
|
|
7851
8121
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
@@ -7967,8 +8237,8 @@ async function toRow(e, v, cwd) {
|
|
|
7967
8237
|
}
|
|
7968
8238
|
|
|
7969
8239
|
// src/commands/controller.ts
|
|
7970
|
-
import
|
|
7971
|
-
import
|
|
8240
|
+
import fs23 from "fs/promises";
|
|
8241
|
+
import path22 from "path";
|
|
7972
8242
|
|
|
7973
8243
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
7974
8244
|
import { createHash } from "crypto";
|
|
@@ -17098,9 +17368,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
17098
17368
|
}
|
|
17099
17369
|
|
|
17100
17370
|
// src/lib/anims.ts
|
|
17101
|
-
import
|
|
17102
|
-
import
|
|
17103
|
-
var ANIMS_DEST =
|
|
17371
|
+
import fs22 from "fs/promises";
|
|
17372
|
+
import path21 from "path";
|
|
17373
|
+
var ANIMS_DEST = path21.join("public", "assets", "anims");
|
|
17104
17374
|
var HIDDEN_TAG = "reference";
|
|
17105
17375
|
async function runAnims(opts) {
|
|
17106
17376
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -17116,7 +17386,7 @@ async function runAnims(opts) {
|
|
|
17116
17386
|
printCatalog(log, manifest, selectors);
|
|
17117
17387
|
return;
|
|
17118
17388
|
}
|
|
17119
|
-
const controllerMarker =
|
|
17389
|
+
const controllerMarker = path21.join(root, "src", "controllers", "character");
|
|
17120
17390
|
if (!await exists2(controllerMarker)) {
|
|
17121
17391
|
log.error(
|
|
17122
17392
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -17125,11 +17395,11 @@ async function runAnims(opts) {
|
|
|
17125
17395
|
process.exitCode = 1;
|
|
17126
17396
|
return;
|
|
17127
17397
|
}
|
|
17128
|
-
const destDir =
|
|
17129
|
-
const gameManifestPath =
|
|
17398
|
+
const destDir = path21.join(root, ANIMS_DEST);
|
|
17399
|
+
const gameManifestPath = path21.join(destDir, "manifest.json");
|
|
17130
17400
|
if (opts.reset) {
|
|
17131
|
-
await
|
|
17132
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
17401
|
+
await fs22.rm(destDir, { recursive: true, force: true });
|
|
17402
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path21.sep)} (--reset)`);
|
|
17133
17403
|
}
|
|
17134
17404
|
if (selectors.length === 0) {
|
|
17135
17405
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -17167,35 +17437,35 @@ async function runAnims(opts) {
|
|
|
17167
17437
|
}
|
|
17168
17438
|
}
|
|
17169
17439
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
17170
|
-
const cacheDir =
|
|
17440
|
+
const cacheDir = path21.join(
|
|
17171
17441
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
17172
17442
|
`${manifest.library}-v${manifest.version}`
|
|
17173
17443
|
);
|
|
17174
|
-
await
|
|
17175
|
-
await
|
|
17444
|
+
await fs22.mkdir(cacheDir, { recursive: true });
|
|
17445
|
+
await fs22.mkdir(destDir, { recursive: true });
|
|
17176
17446
|
const base = getAnimsBase(opts.animsBase);
|
|
17177
17447
|
let installedCount = 0;
|
|
17178
17448
|
let presentCount = 0;
|
|
17179
17449
|
let addedBytes = 0;
|
|
17180
17450
|
const failures = [];
|
|
17181
17451
|
for (const entry of wanted) {
|
|
17182
|
-
const dest =
|
|
17452
|
+
const dest = path21.join(destDir, entry.file);
|
|
17183
17453
|
if (await hasSize(dest, entry.bytes)) {
|
|
17184
17454
|
presentCount++;
|
|
17185
17455
|
continue;
|
|
17186
17456
|
}
|
|
17187
17457
|
try {
|
|
17188
|
-
const cached =
|
|
17458
|
+
const cached = path21.join(cacheDir, entry.file);
|
|
17189
17459
|
if (!await hasSize(cached, entry.bytes)) {
|
|
17190
17460
|
const res = await fetch(base + entry.file);
|
|
17191
17461
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
17192
17462
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
17193
|
-
await
|
|
17463
|
+
await fs22.writeFile(cached, buf);
|
|
17194
17464
|
}
|
|
17195
|
-
await
|
|
17465
|
+
await fs22.copyFile(cached, dest);
|
|
17196
17466
|
installedCount++;
|
|
17197
17467
|
addedBytes += entry.bytes;
|
|
17198
|
-
log.dim(` ${
|
|
17468
|
+
log.dim(` ${path21.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
17199
17469
|
} catch (err) {
|
|
17200
17470
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
17201
17471
|
}
|
|
@@ -17211,13 +17481,13 @@ async function runAnims(opts) {
|
|
|
17211
17481
|
version: manifest.version,
|
|
17212
17482
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
17213
17483
|
};
|
|
17214
|
-
await
|
|
17484
|
+
await fs22.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
17215
17485
|
log.plain("");
|
|
17216
17486
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
17217
17487
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
17218
17488
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
17219
17489
|
log.success(
|
|
17220
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
17490
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path21.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
17221
17491
|
);
|
|
17222
17492
|
for (const [selector, entries] of resolved) {
|
|
17223
17493
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -17249,8 +17519,8 @@ async function loadManifest(baseOverride) {
|
|
|
17249
17519
|
}
|
|
17250
17520
|
} catch {
|
|
17251
17521
|
}
|
|
17252
|
-
const snapshotPath =
|
|
17253
|
-
const manifest = JSON.parse(await
|
|
17522
|
+
const snapshotPath = path21.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
17523
|
+
const manifest = JSON.parse(await fs22.readFile(snapshotPath, "utf8"));
|
|
17254
17524
|
return { manifest, source: "snapshot" };
|
|
17255
17525
|
}
|
|
17256
17526
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -17368,21 +17638,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
17368
17638
|
}
|
|
17369
17639
|
async function readGameManifest(file) {
|
|
17370
17640
|
try {
|
|
17371
|
-
return JSON.parse(await
|
|
17641
|
+
return JSON.parse(await fs22.readFile(file, "utf8"));
|
|
17372
17642
|
} catch {
|
|
17373
17643
|
return null;
|
|
17374
17644
|
}
|
|
17375
17645
|
}
|
|
17376
17646
|
async function hasSize(file, bytes) {
|
|
17377
17647
|
try {
|
|
17378
|
-
return (await
|
|
17648
|
+
return (await fs22.stat(file)).size === bytes;
|
|
17379
17649
|
} catch {
|
|
17380
17650
|
return false;
|
|
17381
17651
|
}
|
|
17382
17652
|
}
|
|
17383
17653
|
async function exists2(p) {
|
|
17384
17654
|
try {
|
|
17385
|
-
await
|
|
17655
|
+
await fs22.access(p);
|
|
17386
17656
|
return true;
|
|
17387
17657
|
} catch {
|
|
17388
17658
|
return false;
|
|
@@ -17576,8 +17846,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
17576
17846
|
]
|
|
17577
17847
|
}
|
|
17578
17848
|
};
|
|
17579
|
-
var CODE_DEST =
|
|
17580
|
-
var ASSETS_DEST =
|
|
17849
|
+
var CODE_DEST = path22.join("src", "controllers");
|
|
17850
|
+
var ASSETS_DEST = path22.join("public", "assets");
|
|
17581
17851
|
async function runController(opts) {
|
|
17582
17852
|
const log = createLogger({ quiet: opts.quiet });
|
|
17583
17853
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -17594,31 +17864,31 @@ async function runController(opts) {
|
|
|
17594
17864
|
process.exitCode = 1;
|
|
17595
17865
|
return;
|
|
17596
17866
|
}
|
|
17597
|
-
const srcDir =
|
|
17867
|
+
const srcDir = path22.join(getTemplatesDir(), "controllers");
|
|
17598
17868
|
const root = opts.cwd ?? process.cwd();
|
|
17599
17869
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
17600
17870
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
17601
17871
|
log.plain("");
|
|
17602
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
17872
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path22.sep)}`);
|
|
17603
17873
|
const plan = [
|
|
17604
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
17874
|
+
...set.code.map((rel) => ({ from: rel, rel: path22.join(CODE_DEST, rel) })),
|
|
17605
17875
|
...set.assets.map((rel) => ({
|
|
17606
17876
|
from: rel,
|
|
17607
|
-
rel:
|
|
17877
|
+
rel: path22.join(ASSETS_DEST, path22.basename(rel))
|
|
17608
17878
|
}))
|
|
17609
17879
|
];
|
|
17610
17880
|
let copied = 0;
|
|
17611
17881
|
let skipped = 0;
|
|
17612
17882
|
try {
|
|
17613
17883
|
for (const file of plan) {
|
|
17614
|
-
const dest =
|
|
17884
|
+
const dest = path22.join(root, file.rel);
|
|
17615
17885
|
if (!opts.force && await exists3(dest)) {
|
|
17616
17886
|
skipped++;
|
|
17617
17887
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
17618
17888
|
continue;
|
|
17619
17889
|
}
|
|
17620
|
-
await
|
|
17621
|
-
await
|
|
17890
|
+
await fs23.mkdir(path22.dirname(dest), { recursive: true });
|
|
17891
|
+
await fs23.copyFile(path22.join(srcDir, file.from), dest);
|
|
17622
17892
|
copied++;
|
|
17623
17893
|
log.dim(` ${file.rel}`);
|
|
17624
17894
|
}
|
|
@@ -17671,7 +17941,7 @@ async function runController(opts) {
|
|
|
17671
17941
|
for (const line of set.sketch) {
|
|
17672
17942
|
log.dim(` ${line}`);
|
|
17673
17943
|
}
|
|
17674
|
-
if (kind === "character" && !await exists3(
|
|
17944
|
+
if (kind === "character" && !await exists3(path22.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
17675
17945
|
log.plain("");
|
|
17676
17946
|
log.plain(
|
|
17677
17947
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -17703,9 +17973,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
17703
17973
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
17704
17974
|
}
|
|
17705
17975
|
assertCompleteMeshyControllerPack(manifest);
|
|
17706
|
-
const destination =
|
|
17707
|
-
await
|
|
17708
|
-
await
|
|
17976
|
+
const destination = path22.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
17977
|
+
await fs23.mkdir(path22.dirname(destination), { recursive: true });
|
|
17978
|
+
await fs23.writeFile(
|
|
17709
17979
|
destination,
|
|
17710
17980
|
`${JSON.stringify(manifest, null, 2)}
|
|
17711
17981
|
`
|
|
@@ -17843,14 +18113,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
17843
18113
|
}
|
|
17844
18114
|
async function installFallbackAvatar(args) {
|
|
17845
18115
|
const { root, srcDir, log } = args;
|
|
17846
|
-
const dest =
|
|
17847
|
-
await
|
|
17848
|
-
await
|
|
18116
|
+
const dest = path22.join(root, ASSETS_DEST, "avatar.vrm");
|
|
18117
|
+
await fs23.mkdir(path22.dirname(dest), { recursive: true });
|
|
18118
|
+
await fs23.copyFile(path22.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
17849
18119
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
17850
18120
|
}
|
|
17851
18121
|
async function exists3(p) {
|
|
17852
18122
|
try {
|
|
17853
|
-
await
|
|
18123
|
+
await fs23.access(p);
|
|
17854
18124
|
return true;
|
|
17855
18125
|
} catch {
|
|
17856
18126
|
return false;
|
|
@@ -17858,8 +18128,8 @@ async function exists3(p) {
|
|
|
17858
18128
|
}
|
|
17859
18129
|
|
|
17860
18130
|
// src/commands/character.ts
|
|
17861
|
-
import
|
|
17862
|
-
import
|
|
18131
|
+
import fs24 from "fs/promises";
|
|
18132
|
+
import path23 from "path";
|
|
17863
18133
|
function exactAnimation(selector) {
|
|
17864
18134
|
const trimmed = selector.trim();
|
|
17865
18135
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -17939,7 +18209,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
17939
18209
|
}
|
|
17940
18210
|
process.exitCode = 1;
|
|
17941
18211
|
}
|
|
17942
|
-
var INSTALLED_MANIFEST =
|
|
18212
|
+
var INSTALLED_MANIFEST = path23.join("public", "assets", "meshy-character.json");
|
|
17943
18213
|
async function resolveAdoptTarget(selector) {
|
|
17944
18214
|
const trimmed = selector?.trim();
|
|
17945
18215
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -17948,7 +18218,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
17948
18218
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
17949
18219
|
let raw;
|
|
17950
18220
|
try {
|
|
17951
|
-
raw = await
|
|
18221
|
+
raw = await fs24.readFile(file, "utf8");
|
|
17952
18222
|
} catch {
|
|
17953
18223
|
return {
|
|
17954
18224
|
ok: false,
|
|
@@ -17979,6 +18249,11 @@ async function runCharacterAdopt(opts) {
|
|
|
17979
18249
|
fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character");
|
|
17980
18250
|
return;
|
|
17981
18251
|
}
|
|
18252
|
+
if (await workspaceMode() === "tools") {
|
|
18253
|
+
reportPlatformRefused(log, "character adopt");
|
|
18254
|
+
process.exitCode = 1;
|
|
18255
|
+
return;
|
|
18256
|
+
}
|
|
17982
18257
|
const project = await readProject();
|
|
17983
18258
|
if (!project?.id) {
|
|
17984
18259
|
fail2(
|
|
@@ -18099,7 +18374,7 @@ async function runWorkflow(args) {
|
|
|
18099
18374
|
return;
|
|
18100
18375
|
}
|
|
18101
18376
|
await recordTerminal(view.id, "completed", (view.files ?? []).map((file) => file.url));
|
|
18102
|
-
await args.completed(view);
|
|
18377
|
+
await args.completed(view, localDeliveryFor(await workspaceMode(), args.opts, args.prompt));
|
|
18103
18378
|
}
|
|
18104
18379
|
async function runCharacterConcept(opts) {
|
|
18105
18380
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
@@ -18322,7 +18597,7 @@ async function runCharacterFinalize(opts) {
|
|
|
18322
18597
|
createPath: basePath,
|
|
18323
18598
|
body,
|
|
18324
18599
|
quote: price,
|
|
18325
|
-
completed: async (view) => {
|
|
18600
|
+
completed: async (view, local) => {
|
|
18326
18601
|
if (opts.json) {
|
|
18327
18602
|
const files = view.files ?? [];
|
|
18328
18603
|
if (files.length === 0) {
|
|
@@ -18348,7 +18623,7 @@ async function runCharacterFinalize(opts) {
|
|
|
18348
18623
|
});
|
|
18349
18624
|
return;
|
|
18350
18625
|
}
|
|
18351
|
-
await reportTerminal("character", view, log);
|
|
18626
|
+
await reportTerminal("character", view, log, false, false, local);
|
|
18352
18627
|
}
|
|
18353
18628
|
});
|
|
18354
18629
|
}
|
|
@@ -18426,22 +18701,22 @@ async function context2(opts) {
|
|
|
18426
18701
|
const project = await readProject();
|
|
18427
18702
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
18428
18703
|
}
|
|
18429
|
-
async function readVideo(
|
|
18704
|
+
async function readVideo(path29, log) {
|
|
18430
18705
|
let bytes;
|
|
18431
18706
|
try {
|
|
18432
|
-
bytes = await readFile(
|
|
18707
|
+
bytes = await readFile(path29);
|
|
18433
18708
|
} catch {
|
|
18434
|
-
log.error(`Can't read ${
|
|
18709
|
+
log.error(`Can't read ${path29}.`);
|
|
18435
18710
|
return null;
|
|
18436
18711
|
}
|
|
18437
18712
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
18438
|
-
log.error(`${basename(
|
|
18713
|
+
log.error(`${basename(path29)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
18439
18714
|
return null;
|
|
18440
18715
|
}
|
|
18441
18716
|
return bytes;
|
|
18442
18717
|
}
|
|
18443
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
18444
|
-
const contentType = /\.mov$/i.test(
|
|
18718
|
+
async function uploadVideo(apiUrl, token, characterId, path29, bytes, log) {
|
|
18719
|
+
const contentType = /\.mov$/i.test(path29) ? "video/quicktime" : "video/mp4";
|
|
18445
18720
|
const minted = await apiFetch(
|
|
18446
18721
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
18447
18722
|
{
|
|
@@ -18456,7 +18731,7 @@ async function uploadVideo(apiUrl, token, characterId, path25, bytes, log) {
|
|
|
18456
18731
|
return null;
|
|
18457
18732
|
}
|
|
18458
18733
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
18459
|
-
log.dim(` uploading ${basename(
|
|
18734
|
+
log.dim(` uploading ${basename(path29)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
18460
18735
|
const put = await fetch(uploadUrl, {
|
|
18461
18736
|
method: "PUT",
|
|
18462
18737
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -18772,8 +19047,8 @@ function rank(items, query) {
|
|
|
18772
19047
|
}
|
|
18773
19048
|
|
|
18774
19049
|
// src/commands/motion.ts
|
|
18775
|
-
import
|
|
18776
|
-
import
|
|
19050
|
+
import fs25 from "fs/promises";
|
|
19051
|
+
import path24 from "path";
|
|
18777
19052
|
|
|
18778
19053
|
// src/lib/motion/npz.ts
|
|
18779
19054
|
import zlib from "zlib";
|
|
@@ -20024,7 +20299,7 @@ async function motionGen(opts, log) {
|
|
|
20024
20299
|
}
|
|
20025
20300
|
if (opts.constraintsPath !== void 0) {
|
|
20026
20301
|
try {
|
|
20027
|
-
const raw = await
|
|
20302
|
+
const raw = await fs25.readFile(opts.constraintsPath, "utf8");
|
|
20028
20303
|
generationOptions.constraints = JSON.parse(raw);
|
|
20029
20304
|
} catch {
|
|
20030
20305
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -20048,10 +20323,10 @@ async function motionGen(opts, log) {
|
|
|
20048
20323
|
async function expandTakes(selectors) {
|
|
20049
20324
|
const out = [];
|
|
20050
20325
|
for (const sel of selectors) {
|
|
20051
|
-
const st = await
|
|
20326
|
+
const st = await fs25.stat(sel).catch(() => null);
|
|
20052
20327
|
if (st?.isDirectory()) {
|
|
20053
|
-
const names = await
|
|
20054
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
20328
|
+
const names = await fs25.readdir(sel);
|
|
20329
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path24.join(sel, n));
|
|
20055
20330
|
} else if (st?.isFile()) {
|
|
20056
20331
|
out.push(sel);
|
|
20057
20332
|
} else {
|
|
@@ -20086,7 +20361,7 @@ async function motionVerify(opts, log) {
|
|
|
20086
20361
|
let gates = DEFAULT_GATES;
|
|
20087
20362
|
if (opts.gatesPath) {
|
|
20088
20363
|
try {
|
|
20089
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
20364
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs25.readFile(opts.gatesPath, "utf8")));
|
|
20090
20365
|
} catch {
|
|
20091
20366
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
20092
20367
|
process.exitCode = 1;
|
|
@@ -20108,9 +20383,9 @@ async function motionVerify(opts, log) {
|
|
|
20108
20383
|
}
|
|
20109
20384
|
const reports = [];
|
|
20110
20385
|
for (const file of files) {
|
|
20111
|
-
const stem =
|
|
20386
|
+
const stem = path24.basename(file).replace(/\.npz$/, "");
|
|
20112
20387
|
try {
|
|
20113
|
-
reports.push(analyzeTake(stem, await
|
|
20388
|
+
reports.push(analyzeTake(stem, await fs25.readFile(file), gates));
|
|
20114
20389
|
} catch (err) {
|
|
20115
20390
|
reports.push({
|
|
20116
20391
|
take: stem,
|
|
@@ -20148,7 +20423,7 @@ async function motionCompile(opts, log) {
|
|
|
20148
20423
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
20149
20424
|
if (opts.configPath) {
|
|
20150
20425
|
try {
|
|
20151
|
-
const patch = JSON.parse(await
|
|
20426
|
+
const patch = JSON.parse(await fs25.readFile(opts.configPath, "utf8"));
|
|
20152
20427
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
20153
20428
|
} catch {
|
|
20154
20429
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -20166,16 +20441,16 @@ async function motionCompile(opts, log) {
|
|
|
20166
20441
|
}
|
|
20167
20442
|
const inputs = [];
|
|
20168
20443
|
for (const file of files) {
|
|
20169
|
-
const stem =
|
|
20444
|
+
const stem = path24.basename(file).replace(/\.npz$/, "");
|
|
20170
20445
|
try {
|
|
20171
|
-
inputs.push({ stem, take: loadTake(await
|
|
20446
|
+
inputs.push({ stem, take: loadTake(await fs25.readFile(file)) });
|
|
20172
20447
|
} catch (err) {
|
|
20173
20448
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20174
20449
|
process.exitCode = 1;
|
|
20175
20450
|
return;
|
|
20176
20451
|
}
|
|
20177
20452
|
}
|
|
20178
|
-
const setName = opts.set ??
|
|
20453
|
+
const setName = opts.set ?? path24.basename(opts.out).replace(/\.json$/, "");
|
|
20179
20454
|
let result;
|
|
20180
20455
|
try {
|
|
20181
20456
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -20190,9 +20465,9 @@ async function motionCompile(opts, log) {
|
|
|
20190
20465
|
process.exitCode = 1;
|
|
20191
20466
|
return;
|
|
20192
20467
|
}
|
|
20193
|
-
await
|
|
20468
|
+
await fs25.mkdir(path24.dirname(path24.resolve(opts.out)), { recursive: true });
|
|
20194
20469
|
const json = JSON.stringify(result.data);
|
|
20195
|
-
await
|
|
20470
|
+
await fs25.writeFile(opts.out, json);
|
|
20196
20471
|
if (opts.json) {
|
|
20197
20472
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
20198
20473
|
return;
|
|
@@ -20210,9 +20485,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
20210
20485
|
var MOTION_PRESETS = {
|
|
20211
20486
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
20212
20487
|
};
|
|
20213
|
-
var MOTION_DEST =
|
|
20488
|
+
var MOTION_DEST = path24.join("src", "motion");
|
|
20214
20489
|
async function motionInstall(opts, log) {
|
|
20215
|
-
const srcDir =
|
|
20490
|
+
const srcDir = path24.join(getTemplatesDir(), "motion");
|
|
20216
20491
|
const root = opts.cwd ?? process.cwd();
|
|
20217
20492
|
const preset = opts.set;
|
|
20218
20493
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -20223,21 +20498,21 @@ async function motionInstall(opts, log) {
|
|
|
20223
20498
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
20224
20499
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
20225
20500
|
log.plain("");
|
|
20226
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
20501
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path24.sep)}`);
|
|
20227
20502
|
let copied = 0, skipped = 0;
|
|
20228
20503
|
try {
|
|
20229
20504
|
for (const rel of files) {
|
|
20230
|
-
const dest =
|
|
20231
|
-
const
|
|
20232
|
-
if (!opts.force &&
|
|
20505
|
+
const dest = path24.join(root, MOTION_DEST, rel);
|
|
20506
|
+
const exists5 = await fs25.access(dest).then(() => true, () => false);
|
|
20507
|
+
if (!opts.force && exists5) {
|
|
20233
20508
|
skipped++;
|
|
20234
|
-
log.dim(` skipped ${
|
|
20509
|
+
log.dim(` skipped ${path24.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
20235
20510
|
continue;
|
|
20236
20511
|
}
|
|
20237
|
-
await
|
|
20238
|
-
await
|
|
20512
|
+
await fs25.mkdir(path24.dirname(dest), { recursive: true });
|
|
20513
|
+
await fs25.copyFile(path24.join(srcDir, rel), dest);
|
|
20239
20514
|
copied++;
|
|
20240
|
-
log.dim(` ${
|
|
20515
|
+
log.dim(` ${path24.join(MOTION_DEST, rel)}`);
|
|
20241
20516
|
}
|
|
20242
20517
|
} catch (err) {
|
|
20243
20518
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -20278,7 +20553,7 @@ async function motionConstraints(opts, log) {
|
|
|
20278
20553
|
}
|
|
20279
20554
|
const doc = directionConstraint(dir, speed, duration);
|
|
20280
20555
|
const out = opts.out ?? "constraints.json";
|
|
20281
|
-
await
|
|
20556
|
+
await fs25.writeFile(out, JSON.stringify(doc));
|
|
20282
20557
|
if (opts.json) {
|
|
20283
20558
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
20284
20559
|
return;
|
|
@@ -20315,9 +20590,9 @@ async function runMotion(opts) {
|
|
|
20315
20590
|
}
|
|
20316
20591
|
|
|
20317
20592
|
// src/commands/asset-new.ts
|
|
20318
|
-
import
|
|
20593
|
+
import fs26 from "fs";
|
|
20319
20594
|
import fsp from "fs/promises";
|
|
20320
|
-
import
|
|
20595
|
+
import path25 from "path";
|
|
20321
20596
|
import { pathToFileURL } from "url";
|
|
20322
20597
|
var EXTRA_FILES = [
|
|
20323
20598
|
"genex-asset.example.json",
|
|
@@ -20411,7 +20686,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
20411
20686
|
}
|
|
20412
20687
|
async function runAssetNew(options) {
|
|
20413
20688
|
const log = createLogger();
|
|
20414
|
-
const cwd = options.dir ?
|
|
20689
|
+
const cwd = options.dir ? path25.resolve(options.dir) : process.cwd();
|
|
20415
20690
|
const slug = options.assetSlug;
|
|
20416
20691
|
if (!slug) {
|
|
20417
20692
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -20421,14 +20696,14 @@ async function runAssetNew(options) {
|
|
|
20421
20696
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
20422
20697
|
return 1;
|
|
20423
20698
|
}
|
|
20424
|
-
const templateDir =
|
|
20425
|
-
if (!
|
|
20699
|
+
const templateDir = path25.join(getTemplatesDir(), "asset-viewer");
|
|
20700
|
+
if (!fs26.existsSync(templateDir)) {
|
|
20426
20701
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
20427
20702
|
return 1;
|
|
20428
20703
|
}
|
|
20429
|
-
const manifestTools = await import(pathToFileURL(
|
|
20704
|
+
const manifestTools = await import(pathToFileURL(path25.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
20430
20705
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
20431
|
-
const lockPath =
|
|
20706
|
+
const lockPath = path25.join(templateDir, "shared-files.sha256.json");
|
|
20432
20707
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
20433
20708
|
const actual = hashSharedFiles(templateDir);
|
|
20434
20709
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -20441,8 +20716,8 @@ async function runAssetNew(options) {
|
|
|
20441
20716
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
20442
20717
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
20443
20718
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
20444
|
-
const outDir =
|
|
20445
|
-
if (
|
|
20719
|
+
const outDir = path25.resolve(cwd, options.out ?? slug);
|
|
20720
|
+
if (fs26.existsSync(outDir) && fs26.readdirSync(outDir).length > 0 && !options.force) {
|
|
20446
20721
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
20447
20722
|
return 1;
|
|
20448
20723
|
}
|
|
@@ -20470,24 +20745,24 @@ async function runAssetNew(options) {
|
|
|
20470
20745
|
};
|
|
20471
20746
|
await fsp.mkdir(outDir, { recursive: true });
|
|
20472
20747
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
20473
|
-
const to =
|
|
20474
|
-
await fsp.mkdir(
|
|
20475
|
-
await fsp.copyFile(
|
|
20748
|
+
const to = path25.join(outDir, rel);
|
|
20749
|
+
await fsp.mkdir(path25.dirname(to), { recursive: true });
|
|
20750
|
+
await fsp.copyFile(path25.join(templateDir, rel), to);
|
|
20476
20751
|
}
|
|
20477
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
20752
|
+
const pkg = fillTemplate(await fsp.readFile(path25.join(templateDir, "package.json"), "utf8"), {
|
|
20478
20753
|
slug,
|
|
20479
20754
|
name,
|
|
20480
20755
|
version
|
|
20481
20756
|
});
|
|
20482
|
-
await fsp.writeFile(
|
|
20483
|
-
await fsp.writeFile(
|
|
20484
|
-
await fsp.writeFile(
|
|
20757
|
+
await fsp.writeFile(path25.join(outDir, "package.json"), pkg, "utf8");
|
|
20758
|
+
await fsp.writeFile(path25.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
20759
|
+
await fsp.writeFile(path25.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
20485
20760
|
await fsp.writeFile(
|
|
20486
|
-
|
|
20761
|
+
path25.join(outDir, "DESIGN.md"),
|
|
20487
20762
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
20488
20763
|
"utf8"
|
|
20489
20764
|
);
|
|
20490
|
-
const placeholder = await fsp.readFile(
|
|
20765
|
+
const placeholder = await fsp.readFile(path25.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
20491
20766
|
const seeded = seedAssetSource(placeholder, {
|
|
20492
20767
|
slug,
|
|
20493
20768
|
name,
|
|
@@ -20498,8 +20773,8 @@ async function runAssetNew(options) {
|
|
|
20498
20773
|
pascalCase
|
|
20499
20774
|
});
|
|
20500
20775
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
20501
|
-
await fsp.mkdir(
|
|
20502
|
-
await fsp.writeFile(
|
|
20776
|
+
await fsp.mkdir(path25.join(outDir, "src", "asset"), { recursive: true });
|
|
20777
|
+
await fsp.writeFile(path25.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
20503
20778
|
const copied = hashSharedFiles(outDir);
|
|
20504
20779
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
20505
20780
|
if (mismatched.length) {
|
|
@@ -20507,7 +20782,7 @@ async function runAssetNew(options) {
|
|
|
20507
20782
|
return 1;
|
|
20508
20783
|
}
|
|
20509
20784
|
await fsp.writeFile(
|
|
20510
|
-
|
|
20785
|
+
path25.join(outDir, PARITY_FILENAME),
|
|
20511
20786
|
JSON.stringify(
|
|
20512
20787
|
{
|
|
20513
20788
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -20550,6 +20825,530 @@ async function runAssetNew(options) {
|
|
|
20550
20825
|
return 0;
|
|
20551
20826
|
}
|
|
20552
20827
|
|
|
20828
|
+
// src/commands/tools.ts
|
|
20829
|
+
import path28 from "path";
|
|
20830
|
+
|
|
20831
|
+
// src/lib/telemetry.ts
|
|
20832
|
+
async function reportToolsInit(apiUrl, token) {
|
|
20833
|
+
if (!token || telemetryDisabled()) return;
|
|
20834
|
+
try {
|
|
20835
|
+
await apiFetch(`${apiUrl}/api/cli/events`, {
|
|
20836
|
+
method: "POST",
|
|
20837
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
20838
|
+
body: JSON.stringify({ event: "tools_init" }),
|
|
20839
|
+
signal: AbortSignal.timeout(2e3)
|
|
20840
|
+
});
|
|
20841
|
+
} catch {
|
|
20842
|
+
}
|
|
20843
|
+
}
|
|
20844
|
+
|
|
20845
|
+
// src/lib/local-install.ts
|
|
20846
|
+
import fs27 from "fs/promises";
|
|
20847
|
+
import path26 from "path";
|
|
20848
|
+
import { spawn as spawn6 } from "child_process";
|
|
20849
|
+
var CLI_PACKAGE = "@genex-ai/cli-demo";
|
|
20850
|
+
var FULL_NAME_FALLBACK = `npx ${CLI_PACKAGE}@${CLI_CHANNEL}`;
|
|
20851
|
+
var LOCKFILES = [
|
|
20852
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
20853
|
+
["yarn.lock", "yarn"],
|
|
20854
|
+
["bun.lockb", "bun"],
|
|
20855
|
+
["bun.lock", "bun"],
|
|
20856
|
+
["package-lock.json", "npm"]
|
|
20857
|
+
];
|
|
20858
|
+
async function exists4(p) {
|
|
20859
|
+
try {
|
|
20860
|
+
await fs27.access(p);
|
|
20861
|
+
return true;
|
|
20862
|
+
} catch {
|
|
20863
|
+
return false;
|
|
20864
|
+
}
|
|
20865
|
+
}
|
|
20866
|
+
async function detectPackageManager(cwd) {
|
|
20867
|
+
let dir = path26.resolve(cwd);
|
|
20868
|
+
for (; ; ) {
|
|
20869
|
+
try {
|
|
20870
|
+
const raw = await fs27.readFile(path26.join(dir, "package.json"), "utf8");
|
|
20871
|
+
const pm = JSON.parse(raw).packageManager;
|
|
20872
|
+
if (typeof pm === "string") {
|
|
20873
|
+
const name = pm.split("@")[0];
|
|
20874
|
+
if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") return name;
|
|
20875
|
+
}
|
|
20876
|
+
} catch {
|
|
20877
|
+
}
|
|
20878
|
+
for (const [file, pm] of LOCKFILES) {
|
|
20879
|
+
if (await exists4(path26.join(dir, file))) return pm;
|
|
20880
|
+
}
|
|
20881
|
+
const parent = path26.dirname(dir);
|
|
20882
|
+
if (parent === dir) return "npm";
|
|
20883
|
+
dir = parent;
|
|
20884
|
+
}
|
|
20885
|
+
}
|
|
20886
|
+
async function findLocalCli(cwd) {
|
|
20887
|
+
let dir = path26.resolve(cwd);
|
|
20888
|
+
for (; ; ) {
|
|
20889
|
+
if (await exists4(path26.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
|
|
20890
|
+
const parent = path26.dirname(dir);
|
|
20891
|
+
if (parent === dir) return null;
|
|
20892
|
+
dir = parent;
|
|
20893
|
+
}
|
|
20894
|
+
}
|
|
20895
|
+
function installArgs(pm, spec) {
|
|
20896
|
+
switch (pm) {
|
|
20897
|
+
case "pnpm":
|
|
20898
|
+
return { command: "pnpm", args: ["add", "-D", spec] };
|
|
20899
|
+
case "yarn":
|
|
20900
|
+
return { command: "yarn", args: ["add", "-D", spec] };
|
|
20901
|
+
case "bun":
|
|
20902
|
+
return { command: "bun", args: ["add", "-d", spec] };
|
|
20903
|
+
case "npm":
|
|
20904
|
+
return { command: "npm", args: ["install", "--save-dev", "--no-audit", "--no-fund", spec] };
|
|
20905
|
+
}
|
|
20906
|
+
}
|
|
20907
|
+
function manifestName(cwd) {
|
|
20908
|
+
const slug = path26.basename(path26.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
20909
|
+
return slug || "genex-tools-workspace";
|
|
20910
|
+
}
|
|
20911
|
+
function isSourceRun(moduleUrl = import.meta.url) {
|
|
20912
|
+
return moduleUrl.endsWith(".ts");
|
|
20913
|
+
}
|
|
20914
|
+
var INSTALL_TIMEOUT_MS = 18e4;
|
|
20915
|
+
async function spawnInstall(command, args, cwd) {
|
|
20916
|
+
return new Promise((resolve) => {
|
|
20917
|
+
let output = "";
|
|
20918
|
+
let settled = false;
|
|
20919
|
+
const done = (ok, detail) => {
|
|
20920
|
+
if (settled) return;
|
|
20921
|
+
settled = true;
|
|
20922
|
+
resolve({ ok, detail });
|
|
20923
|
+
};
|
|
20924
|
+
try {
|
|
20925
|
+
const child = spawn6(command, args, {
|
|
20926
|
+
cwd,
|
|
20927
|
+
// npm/pnpm/yarn are .cmd shims on Windows; the args carry no user input.
|
|
20928
|
+
shell: process.platform === "win32",
|
|
20929
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
20930
|
+
});
|
|
20931
|
+
const timer = setTimeout(() => {
|
|
20932
|
+
child.kill();
|
|
20933
|
+
done(false, `timed out after ${INSTALL_TIMEOUT_MS / 1e3}s`);
|
|
20934
|
+
}, INSTALL_TIMEOUT_MS);
|
|
20935
|
+
child.stdout?.on("data", (d) => output += d.toString());
|
|
20936
|
+
child.stderr?.on("data", (d) => output += d.toString());
|
|
20937
|
+
child.on("error", (err) => {
|
|
20938
|
+
clearTimeout(timer);
|
|
20939
|
+
done(false, err.message);
|
|
20940
|
+
});
|
|
20941
|
+
child.on("close", (code2) => {
|
|
20942
|
+
clearTimeout(timer);
|
|
20943
|
+
if (code2 === 0) done(true);
|
|
20944
|
+
else done(false, output.trim().split("\n").slice(-3).join("\n"));
|
|
20945
|
+
});
|
|
20946
|
+
} catch (err) {
|
|
20947
|
+
done(false, err instanceof Error ? err.message : String(err));
|
|
20948
|
+
}
|
|
20949
|
+
});
|
|
20950
|
+
}
|
|
20951
|
+
async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
|
|
20952
|
+
if (opts.skip) {
|
|
20953
|
+
log.dim(` --no-install: commands are spelled ${c.cyan(FULL_NAME_FALLBACK)} without it.`);
|
|
20954
|
+
return;
|
|
20955
|
+
}
|
|
20956
|
+
if (!opts.runner && isSourceRun()) {
|
|
20957
|
+
log.dim(" (source run \u2014 skipping the local CLI install)");
|
|
20958
|
+
return;
|
|
20959
|
+
}
|
|
20960
|
+
const already = await findLocalCli(cwd);
|
|
20961
|
+
if (already) {
|
|
20962
|
+
log.dim(` ${CLI_PACKAGE} already resolves from ${c.cyan(already)} \u2014 npx genex works here.`);
|
|
20963
|
+
return;
|
|
20964
|
+
}
|
|
20965
|
+
const pm = await detectPackageManager(cwd);
|
|
20966
|
+
const hadManifest = await exists4(path26.join(cwd, "package.json"));
|
|
20967
|
+
if (!hadManifest) {
|
|
20968
|
+
await fs27.writeFile(
|
|
20969
|
+
path26.join(cwd, "package.json"),
|
|
20970
|
+
JSON.stringify({ name: manifestName(cwd), private: true }, null, 2) + "\n"
|
|
20971
|
+
);
|
|
20972
|
+
await ensureIgnored(cwd, "node_modules/");
|
|
20973
|
+
log.dim(" wrote a minimal package.json (none existed \u2014 the install must land HERE, not in a parent)");
|
|
20974
|
+
}
|
|
20975
|
+
const spec = `${CLI_PACKAGE}@${CLI_CHANNEL}`;
|
|
20976
|
+
const { command, args } = installArgs(pm, spec);
|
|
20977
|
+
log.step(`Adding ${c.cyan(CLI_PACKAGE)} as a devDependency (${pm}) so npx genex resolves here\u2026`);
|
|
20978
|
+
const run2 = opts.runner ?? spawnInstall;
|
|
20979
|
+
const result = await run2(command, args, cwd);
|
|
20980
|
+
if (result.ok) {
|
|
20981
|
+
log.success(`${c.cyan("npx genex")} now resolves in this folder.`);
|
|
20982
|
+
} else {
|
|
20983
|
+
log.warn(`Couldn't install ${CLI_PACKAGE} via ${pm}${result.detail ? ` \u2014 ${result.detail}` : ""}.`);
|
|
20984
|
+
log.dim(` Everything still works with the full name: ${c.cyan(`${FULL_NAME_FALLBACK} doctor`)}`);
|
|
20985
|
+
log.dim(` Or add it yourself: ${c.cyan(`${command} ${args.join(" ")}`)}`);
|
|
20986
|
+
}
|
|
20987
|
+
}
|
|
20988
|
+
async function ensureIgnored(dir, entry) {
|
|
20989
|
+
const file = path26.join(dir, ".gitignore");
|
|
20990
|
+
let content = "";
|
|
20991
|
+
try {
|
|
20992
|
+
content = await fs27.readFile(file, "utf8");
|
|
20993
|
+
} catch {
|
|
20994
|
+
}
|
|
20995
|
+
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
20996
|
+
let next = content;
|
|
20997
|
+
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
20998
|
+
await fs27.writeFile(file, next + entry + "\n");
|
|
20999
|
+
}
|
|
21000
|
+
|
|
21001
|
+
// src/commands/doctor.ts
|
|
21002
|
+
import path27 from "path";
|
|
21003
|
+
var LANE_ORDER = [
|
|
21004
|
+
"model",
|
|
21005
|
+
"image",
|
|
21006
|
+
"video",
|
|
21007
|
+
"texture",
|
|
21008
|
+
"sfx",
|
|
21009
|
+
"music",
|
|
21010
|
+
"voice",
|
|
21011
|
+
"character",
|
|
21012
|
+
"character_motion",
|
|
21013
|
+
"model_segment",
|
|
21014
|
+
"model_rig",
|
|
21015
|
+
"model_animation"
|
|
21016
|
+
];
|
|
21017
|
+
var NODE_FLOOR = 20;
|
|
21018
|
+
async function runDoctor(opts = {}) {
|
|
21019
|
+
const log = createLogger({ quiet: opts.quiet && !opts.json });
|
|
21020
|
+
const rows = [];
|
|
21021
|
+
const [project, workspace] = await Promise.all([readProject(), readWorkspace()]);
|
|
21022
|
+
const mode = project ? "game" : workspace?.mode === "tools" ? "tools" : null;
|
|
21023
|
+
const synced = await firstSkillsMarker();
|
|
21024
|
+
const syncNote = synced ? ` \xB7 skills ${synced}` : " \xB7 no skills installed here";
|
|
21025
|
+
rows.push(
|
|
21026
|
+
mode === "game" ? { label: "Workspace", value: `genex game "${project.slug}"${syncNote}` } : mode === "tools" ? { label: "Workspace", value: `Genex Tools workspace${syncNote}` } : {
|
|
21027
|
+
label: "Workspace",
|
|
21028
|
+
value: "not a Genex folder",
|
|
21029
|
+
// Never `bad`: doctor is exactly what someone runs BEFORE setting
|
|
21030
|
+
// up, and a red mark there reads as a broken install.
|
|
21031
|
+
fix: "`npx genex tools` for the asset toolkit, or `npx genex init` to build a hosted game here."
|
|
21032
|
+
}
|
|
21033
|
+
);
|
|
21034
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
21035
|
+
const nodeOk = Number.isFinite(nodeMajor) && nodeMajor >= NODE_FLOOR;
|
|
21036
|
+
rows.push({
|
|
21037
|
+
label: "Node",
|
|
21038
|
+
value: `v${process.versions.node}${nodeOk ? "" : ` (needs \u2265 ${NODE_FLOOR})`}`,
|
|
21039
|
+
bad: !nodeOk,
|
|
21040
|
+
fix: nodeOk ? void 0 : "Install Node 20 or newer: nodejs.org/download (or `brew install node`)."
|
|
21041
|
+
});
|
|
21042
|
+
const installed = getCliVersion();
|
|
21043
|
+
const latest = await latestPublished();
|
|
21044
|
+
rows.push({
|
|
21045
|
+
label: "CLI",
|
|
21046
|
+
value: latest && isNewerVersion(latest, installed) ? `${installed} (latest on npm: ${latest})` : `${installed}${latest ? " (up to date)" : ""}`,
|
|
21047
|
+
// An outdated CLI is a nudge, never a failure — it is the update machinery's
|
|
21048
|
+
// job to say so, and failing here would make `doctor` red on every machine
|
|
21049
|
+
// the morning after a release.
|
|
21050
|
+
fix: latest && isNewerVersion(latest, installed) ? "npm i -D @genex-ai/cli-demo@latest (apply at a safe moment, never mid-task)" : void 0
|
|
21051
|
+
});
|
|
21052
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? workspace?.apiUrl ?? project?.apiUrl);
|
|
21053
|
+
const token = await readUserToken(opts.envPath);
|
|
21054
|
+
let email = null;
|
|
21055
|
+
let authState = "none";
|
|
21056
|
+
if (token) {
|
|
21057
|
+
const probe = await probeSession(apiUrl, token);
|
|
21058
|
+
authState = probe.state;
|
|
21059
|
+
email = probe.email;
|
|
21060
|
+
}
|
|
21061
|
+
rows.push(authRow(authState, email, opts.envPath));
|
|
21062
|
+
let credits = null;
|
|
21063
|
+
let lanes = null;
|
|
21064
|
+
if (authState === "ok" && token) {
|
|
21065
|
+
[credits, lanes] = await Promise.all([fetchCredits(apiUrl, token), fetchLanes(apiUrl, token)]);
|
|
21066
|
+
}
|
|
21067
|
+
rows.push(creditsRow(authState, credits));
|
|
21068
|
+
if (opts.json) {
|
|
21069
|
+
process.stdout.write(
|
|
21070
|
+
JSON.stringify(
|
|
21071
|
+
{
|
|
21072
|
+
workspace: mode,
|
|
21073
|
+
node: process.versions.node,
|
|
21074
|
+
cli: { installed, latest },
|
|
21075
|
+
auth: { state: authState, email, apiUrl },
|
|
21076
|
+
credits,
|
|
21077
|
+
lanes: lanes ? { paused: lanes.paused, lanes: lanes.lanes } : null,
|
|
21078
|
+
ok: !rows.some((r) => r.bad)
|
|
21079
|
+
},
|
|
21080
|
+
null,
|
|
21081
|
+
2
|
|
21082
|
+
) + "\n"
|
|
21083
|
+
);
|
|
21084
|
+
if (rows.some((r) => r.bad)) process.exitCode = 1;
|
|
21085
|
+
return;
|
|
21086
|
+
}
|
|
21087
|
+
log.plain(c.bold("genex doctor"));
|
|
21088
|
+
log.dim(` ${apiUrl}`);
|
|
21089
|
+
log.plain("");
|
|
21090
|
+
for (const row of rows) {
|
|
21091
|
+
const mark = row.bad ? c.red("\u2717") : c.green("\u2713");
|
|
21092
|
+
log.plain(` ${mark} ${row.label.padEnd(11)}${row.value}`);
|
|
21093
|
+
if (row.fix) log.dim(` ${row.fix}`);
|
|
21094
|
+
}
|
|
21095
|
+
let laneProblem = false;
|
|
21096
|
+
if (authState === "ok") {
|
|
21097
|
+
log.plain("");
|
|
21098
|
+
if (!lanes) {
|
|
21099
|
+
log.plain(` ${c.yellow("?")} Lanes couldn't be read from this server`);
|
|
21100
|
+
log.dim(
|
|
21101
|
+
" An older server has no lane report \u2014 generation still works, and a lane with no"
|
|
21102
|
+
);
|
|
21103
|
+
log.dim(" provider key would return a placeholder asset instead of failing.");
|
|
21104
|
+
} else if (lanes.paused) {
|
|
21105
|
+
laneProblem = true;
|
|
21106
|
+
log.plain(` ${c.red("\u2717")} Lanes generation is PAUSED platform-wide`);
|
|
21107
|
+
log.dim(" The daily spend breaker is tripped \u2014 nothing generates until it resets.");
|
|
21108
|
+
} else {
|
|
21109
|
+
const mocks = lanes.lanes.filter((l) => l.mock);
|
|
21110
|
+
const mark = mocks.length > 0 ? c.yellow("!") : c.green("\u2713");
|
|
21111
|
+
log.plain(
|
|
21112
|
+
` ${mark} Lanes ${lanes.lanes.length - mocks.length}/${lanes.lanes.length} live${mocks.length > 0 ? ` \xB7 ${mocks.length} in SAMPLE mode` : ""}`
|
|
21113
|
+
);
|
|
21114
|
+
for (const kind of laneOrder(lanes)) {
|
|
21115
|
+
const lane = lanes.lanes.find((l) => l.kind === kind);
|
|
21116
|
+
if (!lane) continue;
|
|
21117
|
+
const price = credits?.prices?.[kind];
|
|
21118
|
+
const cost = typeof price === "number" ? `${price} cr` : "";
|
|
21119
|
+
if (lane.mock) {
|
|
21120
|
+
laneProblem = true;
|
|
21121
|
+
log.plain(
|
|
21122
|
+
` ${c.yellow("!")} ${kind.padEnd(17)}${"SAMPLE (no provider key)".padEnd(26)}${cost}`
|
|
21123
|
+
);
|
|
21124
|
+
} else {
|
|
21125
|
+
log.dim(` \xB7 ${kind.padEnd(17)}${`live (${lane.provider})`.padEnd(26)}${cost}`);
|
|
21126
|
+
}
|
|
21127
|
+
}
|
|
21128
|
+
if (mocks.length > 0) {
|
|
21129
|
+
log.dim(
|
|
21130
|
+
" A SAMPLE lane returns a placeholder asset and still bills. In a Genex Tools"
|
|
21131
|
+
);
|
|
21132
|
+
log.dim(" workspace those are refused before anything is charged.");
|
|
21133
|
+
}
|
|
21134
|
+
}
|
|
21135
|
+
}
|
|
21136
|
+
const failed = rows.some((r) => r.bad) || laneProblem && mode !== "game";
|
|
21137
|
+
log.plain("");
|
|
21138
|
+
if (failed) {
|
|
21139
|
+
log.plain(` ${c.red("Something above needs fixing.")}`);
|
|
21140
|
+
process.exitCode = 1;
|
|
21141
|
+
} else {
|
|
21142
|
+
log.plain(` ${c.green("All good.")}`);
|
|
21143
|
+
}
|
|
21144
|
+
}
|
|
21145
|
+
function laneOrder(report) {
|
|
21146
|
+
const known = LANE_ORDER.filter((k) => report.lanes.some((l) => l.kind === k));
|
|
21147
|
+
const extra = report.lanes.map((l) => l.kind).filter((k) => !LANE_ORDER.includes(k));
|
|
21148
|
+
return [...known, ...extra.sort()];
|
|
21149
|
+
}
|
|
21150
|
+
function authRow(state, email, envPath) {
|
|
21151
|
+
switch (state) {
|
|
21152
|
+
case "ok":
|
|
21153
|
+
return { label: "Auth", value: `connected as ${email ?? "this account"}` };
|
|
21154
|
+
case "rejected":
|
|
21155
|
+
return {
|
|
21156
|
+
label: "Auth",
|
|
21157
|
+
value: "your saved sign-in was rejected",
|
|
21158
|
+
bad: true,
|
|
21159
|
+
fix: `The credential in ${getGenexEnvPath(envPath)} is expired or revoked \u2014 run \`npx genex auth --force\`.`
|
|
21160
|
+
};
|
|
21161
|
+
case "offline":
|
|
21162
|
+
return {
|
|
21163
|
+
label: "Auth",
|
|
21164
|
+
value: "couldn't reach the API to check",
|
|
21165
|
+
bad: true,
|
|
21166
|
+
fix: "Check the network, then re-run. Nothing is wrong with your credential yet."
|
|
21167
|
+
};
|
|
21168
|
+
default:
|
|
21169
|
+
return {
|
|
21170
|
+
label: "Auth",
|
|
21171
|
+
value: "not signed in",
|
|
21172
|
+
bad: true,
|
|
21173
|
+
fix: "`npx genex auth` \u2014 it prints a short link and a code; approve it on any device."
|
|
21174
|
+
};
|
|
21175
|
+
}
|
|
21176
|
+
}
|
|
21177
|
+
function creditsRow(state, info) {
|
|
21178
|
+
if (state !== "ok") return { label: "Credits", value: "\u2014 (sign in first)" };
|
|
21179
|
+
if (!info) return { label: "Credits", value: "couldn't be read from this server" };
|
|
21180
|
+
if (info.unlimited) return { label: "Credits", value: "unlimited (operator account)" };
|
|
21181
|
+
if (!info.emailVerified) {
|
|
21182
|
+
return {
|
|
21183
|
+
label: "Credits",
|
|
21184
|
+
value: `${info.balance} \xB7 email not verified`,
|
|
21185
|
+
bad: true,
|
|
21186
|
+
fix: "Verify your email to unlock your free credits \u2014 then re-run."
|
|
21187
|
+
};
|
|
21188
|
+
}
|
|
21189
|
+
const refill = info.refillAt ? ` \xB7 refills to ${info.refillTo} on ${shortDate2(info.refillAt)}` : "";
|
|
21190
|
+
return {
|
|
21191
|
+
label: "Credits",
|
|
21192
|
+
value: `${info.balance}${refill}`,
|
|
21193
|
+
bad: info.balance <= 0,
|
|
21194
|
+
fix: info.balance <= 0 ? "Out of credits \u2014 generation is refused until the refill or a top-up." : void 0
|
|
21195
|
+
};
|
|
21196
|
+
}
|
|
21197
|
+
function shortDate2(iso) {
|
|
21198
|
+
const d = new Date(iso);
|
|
21199
|
+
return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
21200
|
+
}
|
|
21201
|
+
async function probeSession(apiUrl, token) {
|
|
21202
|
+
try {
|
|
21203
|
+
const res = await apiFetch(`${apiUrl}/api/auth/get-session`, {
|
|
21204
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
21205
|
+
signal: AbortSignal.timeout(6e3)
|
|
21206
|
+
});
|
|
21207
|
+
if (res.status === 401 || res.status === 403) return { state: "rejected", email: null };
|
|
21208
|
+
if (!res.ok) return { state: "offline", email: null };
|
|
21209
|
+
const data = await res.json().catch(() => null);
|
|
21210
|
+
if (!data?.user) return { state: "rejected", email: null };
|
|
21211
|
+
return { state: "ok", email: data.user.email ?? null };
|
|
21212
|
+
} catch {
|
|
21213
|
+
return { state: "offline", email: null };
|
|
21214
|
+
}
|
|
21215
|
+
}
|
|
21216
|
+
async function fetchCredits(apiUrl, token) {
|
|
21217
|
+
try {
|
|
21218
|
+
const res = await apiFetch(`${apiUrl}/api/credits/me`, {
|
|
21219
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
21220
|
+
signal: AbortSignal.timeout(6e3)
|
|
21221
|
+
});
|
|
21222
|
+
if (!res.ok) return null;
|
|
21223
|
+
return await res.json();
|
|
21224
|
+
} catch {
|
|
21225
|
+
return null;
|
|
21226
|
+
}
|
|
21227
|
+
}
|
|
21228
|
+
async function firstSkillsMarker() {
|
|
21229
|
+
for (const target of resolveAgentTargets()) {
|
|
21230
|
+
const marker = await readSkillsMarker(path27.join(target.baseDir, "skills"));
|
|
21231
|
+
if (marker) return marker;
|
|
21232
|
+
}
|
|
21233
|
+
return null;
|
|
21234
|
+
}
|
|
21235
|
+
async function latestPublished() {
|
|
21236
|
+
try {
|
|
21237
|
+
const cache = await startUpdateCheck();
|
|
21238
|
+
return cache?.latest["@genex-ai/cli-demo"] ?? null;
|
|
21239
|
+
} catch {
|
|
21240
|
+
return null;
|
|
21241
|
+
}
|
|
21242
|
+
}
|
|
21243
|
+
|
|
21244
|
+
// src/commands/tools.ts
|
|
21245
|
+
async function runTools(opts) {
|
|
21246
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
21247
|
+
const project = await readProject();
|
|
21248
|
+
if (project) {
|
|
21249
|
+
log.error("This folder is already a Genex game project.");
|
|
21250
|
+
log.plain(
|
|
21251
|
+
[
|
|
21252
|
+
` Its skills already cover every generation tool \u2014 ${c.cyan("npx genex --help")} lists them,`,
|
|
21253
|
+
" and it can publish, host and go multiplayer on top. Nothing to set up.",
|
|
21254
|
+
" Genex Tools is for a game you host yourself; run it in that project's folder."
|
|
21255
|
+
].join("\n")
|
|
21256
|
+
);
|
|
21257
|
+
process.exitCode = 1;
|
|
21258
|
+
return;
|
|
21259
|
+
}
|
|
21260
|
+
log.plain(c.bold("genex tools"));
|
|
21261
|
+
log.plain("");
|
|
21262
|
+
await cleanupLegacyGlobalSkills(log);
|
|
21263
|
+
const templatesDir = getTemplatesDir();
|
|
21264
|
+
const targets = resolveAgentTargets({ dir: opts.dir, agents: opts.agents });
|
|
21265
|
+
log.step(`Installing Genex tool skills for: ${targets.map((t) => c.cyan(t.label)).join(", ")}`);
|
|
21266
|
+
let totalNew = 0;
|
|
21267
|
+
let totalUpdated = 0;
|
|
21268
|
+
for (const t of targets) {
|
|
21269
|
+
const dest = path28.join(t.baseDir, "skills");
|
|
21270
|
+
const { copied, updated } = await copyTemplates(path28.join(templatesDir, "skills"), dest, {
|
|
21271
|
+
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
21272
|
+
});
|
|
21273
|
+
await pruneRemovedSkills(dest, log);
|
|
21274
|
+
await writeSkillsMarker(dest);
|
|
21275
|
+
const added = copied.length - updated.length;
|
|
21276
|
+
totalNew += added;
|
|
21277
|
+
totalUpdated += updated.length;
|
|
21278
|
+
log.dim(` ${t.label}: ${added} added, ${updated.length} refreshed \u2192 ${dest}`);
|
|
21279
|
+
}
|
|
21280
|
+
log.success(
|
|
21281
|
+
`Tool skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
|
|
21282
|
+
);
|
|
21283
|
+
const apiUrl = getApiUrl(opts.apiUrl);
|
|
21284
|
+
const { path: markerPath } = await writeWorkspace({
|
|
21285
|
+
mode: "tools",
|
|
21286
|
+
apiUrl,
|
|
21287
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21288
|
+
});
|
|
21289
|
+
log.dim(` saved ${c.cyan(markerPath)}`);
|
|
21290
|
+
await writeGitignore(process.cwd(), log);
|
|
21291
|
+
if (await writeToolsContract(process.cwd())) {
|
|
21292
|
+
log.dim(" wrote the Genex Tools rules into AGENTS.md (managed block) + CLAUDE.md stub");
|
|
21293
|
+
}
|
|
21294
|
+
await healAncestorContractsAndReport(log, process.cwd());
|
|
21295
|
+
await ensureLocalCli(log, process.cwd(), { skip: opts.noInstall });
|
|
21296
|
+
log.plain("");
|
|
21297
|
+
const authBaseUrl = getAuthUrl(opts.authUrl);
|
|
21298
|
+
let token = await readUserToken(opts.envPath);
|
|
21299
|
+
if (!token) {
|
|
21300
|
+
try {
|
|
21301
|
+
token = await authorize(apiUrl, authBaseUrl, {
|
|
21302
|
+
log,
|
|
21303
|
+
inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
21304
|
+
});
|
|
21305
|
+
} catch (err) {
|
|
21306
|
+
if (err instanceof AuthPendingError) {
|
|
21307
|
+
printAuthHandoff(log, err);
|
|
21308
|
+
log.dim(
|
|
21309
|
+
"Everything else is set up \u2014 `genex auth` finishes the sign-in, then you can generate."
|
|
21310
|
+
);
|
|
21311
|
+
return;
|
|
21312
|
+
}
|
|
21313
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
21314
|
+
log.dim("The workspace is set up. Re-run `genex tools` to finish authorizing.");
|
|
21315
|
+
throw markPrinted(err);
|
|
21316
|
+
}
|
|
21317
|
+
const { path: tokenPath } = await writeUserToken(token, opts.envPath);
|
|
21318
|
+
log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
|
|
21319
|
+
log.plain("");
|
|
21320
|
+
}
|
|
21321
|
+
await reportToolsInit(apiUrl, token);
|
|
21322
|
+
await runDoctor({ apiUrl: opts.apiUrl, envPath: opts.envPath, quiet: opts.quiet });
|
|
21323
|
+
log.plain("");
|
|
21324
|
+
log.success("Genex Tools is ready. \u{1F9F0}");
|
|
21325
|
+
log.plain(` Try it: ${c.cyan('npx genex image "weathered treasure map, hand-inked" --transparent')}`);
|
|
21326
|
+
log.dim(" Assets land in ./assets/ as files you own \u2014 wire the local path into your game.");
|
|
21327
|
+
}
|
|
21328
|
+
|
|
21329
|
+
// src/lib/costs.ts
|
|
21330
|
+
var TYPICAL_CREDITS = {
|
|
21331
|
+
model: 25,
|
|
21332
|
+
image: 8,
|
|
21333
|
+
texture: 3,
|
|
21334
|
+
sfx: 5,
|
|
21335
|
+
music: 40,
|
|
21336
|
+
voice: 10,
|
|
21337
|
+
video: 20,
|
|
21338
|
+
model_segment: 30,
|
|
21339
|
+
model_rig: 20,
|
|
21340
|
+
model_animation: 8,
|
|
21341
|
+
// PER CLIP — `--preset walk,run` bills twice
|
|
21342
|
+
character: 50,
|
|
21343
|
+
character_concept: 15,
|
|
21344
|
+
character_preview: 20,
|
|
21345
|
+
character_finalize: 30,
|
|
21346
|
+
character_animation: 5,
|
|
21347
|
+
character_motion: 26,
|
|
21348
|
+
// PER CLIP on the text route; other routes scale from it
|
|
21349
|
+
character_rerig: 10
|
|
21350
|
+
};
|
|
21351
|
+
|
|
20553
21352
|
// src/index.ts
|
|
20554
21353
|
var MODEL_SUB_SET = new Set(MODEL_SUBCOMMANDS);
|
|
20555
21354
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
|
|
@@ -20557,6 +21356,17 @@ var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, aut
|
|
|
20557
21356
|
|
|
20558
21357
|
${c.bold("Usage")}
|
|
20559
21358
|
genex init [<name>] [options] Scaffold + authorize + create the draft project.
|
|
21359
|
+
genex tools [options] Set this folder up as a Genex Tools workspace: the
|
|
21360
|
+
asset generators alone (models, images, video,
|
|
21361
|
+
textures, audio, characters) for a game you host
|
|
21362
|
+
yourself. Installs the tool skill cards, signs you
|
|
21363
|
+
in, and downloads every asset into ./assets.
|
|
21364
|
+
Creates nothing on the platform.
|
|
21365
|
+
genex doctor [--json] Check everything a generation needs: sign-in,
|
|
21366
|
+
credit balance, Node + CLI version, and which
|
|
21367
|
+
generation lanes are live. Exit 0 all clear,
|
|
21368
|
+
1 if something needs fixing \u2014 run it first when
|
|
21369
|
+
a command fails.
|
|
20560
21370
|
genex auth [options] Connect this machine to your Genex account, or
|
|
20561
21371
|
finish a sign-in another command started \u2014 it
|
|
20562
21372
|
resumes the same code, so an interrupted
|
|
@@ -20731,6 +21541,9 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
|
|
|
20731
21541
|
multi-beat move room to land instead of rushing it.
|
|
20732
21542
|
(music: 10-300, default 90 \u2014 a looping gameplay track).
|
|
20733
21543
|
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
21544
|
+
Takes --quality and --size; without --quality it runs a
|
|
21545
|
+
mid tier. Not with --candidates (a cutout is billed per
|
|
21546
|
+
image \u2014 pick a candidate first, then --clean it).
|
|
20734
21547
|
--glass (image) magenta-key glass lane: generate the panel on a
|
|
20735
21548
|
flat #FF00FF screen, then locally solve REAL per-pixel
|
|
20736
21549
|
translucency and write RGBA files (glass-1.png\u2026) into
|
|
@@ -20770,6 +21583,10 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
|
|
|
20770
21583
|
--last-frame <url> (video) \u2026 and its end frame.
|
|
20771
21584
|
--no-wait Enqueue only; don't block waiting for the asset. Prints
|
|
20772
21585
|
the id that "genex wait <id>" picks the result up with.
|
|
21586
|
+
--no-download (Genex Tools workspaces) print the asset URL only instead
|
|
21587
|
+
of saving the file into ./assets.
|
|
21588
|
+
--out-dir <dir> (Genex Tools workspaces) where saved assets land
|
|
21589
|
+
(default ./assets). Also --glass's output directory.
|
|
20773
21590
|
--api-url <url> Override the API base URL.
|
|
20774
21591
|
--env <path> Token env file (default: ~/.genex/env).
|
|
20775
21592
|
|
|
@@ -20935,6 +21752,86 @@ ${c.bold("Examples")}
|
|
|
20935
21752
|
genex controller chat
|
|
20936
21753
|
genex controller voice
|
|
20937
21754
|
`;
|
|
21755
|
+
var cr = (kind) => `${String(TYPICAL_CREDITS[kind] ?? "?").padStart(3)} cr`;
|
|
21756
|
+
var HELP_TOOLS = `${c.bold("genex")} \u2014 the gamedev AI asset toolkit. One sign-in, one meter, no provider accounts.
|
|
21757
|
+
|
|
21758
|
+
This folder is a ${c.bold("Genex Tools")} workspace: generation only. Assets download into
|
|
21759
|
+
${c.cyan("./assets")} as files you own \u2014 wire the local path into your game, whatever engine
|
|
21760
|
+
it uses and wherever you host it.
|
|
21761
|
+
|
|
21762
|
+
${c.bold("Generate")} typical cost
|
|
21763
|
+
genex model "<prompt>" A 3D model (GLB). ${cr("model")}
|
|
21764
|
+
--image <path|url> \u2026built from a reference image instead.
|
|
21765
|
+
genex model segment <id> Split a model into NAMED parts. ${cr("model_segment")}
|
|
21766
|
+
genex model rig <id> Give any mesh a skeleton (7 body ${cr("model_rig")}
|
|
21767
|
+
plans: biped|quadruped|hexapod|
|
|
21768
|
+
octopod|avian|serpentine|aquatic).
|
|
21769
|
+
genex model animate <rig-id> --preset walk[,run,\u2026]
|
|
21770
|
+
Retarget ready-made clips onto a rig. ${cr("model_animation")} each
|
|
21771
|
+
genex image "<prompt>" An image (PNG/JPEG). ${cr("image")}
|
|
21772
|
+
--transparent \u2026with a real alpha channel (sprites, icons, logos).
|
|
21773
|
+
--edit <path|url> \u2026edit an existing image with the prompt.
|
|
21774
|
+
--remove-bg / --bg-mode <m> \u2026cut the background (sprite|glyph|sheet|matte).
|
|
21775
|
+
genex video "<prompt>" A video (mp4). --loop, --frame <img>. ${cr("video")}
|
|
21776
|
+
genex texture "<prompt>" A tiling PBR texture set. --terrain. ${cr("texture")}
|
|
21777
|
+
genex sfx "<prompt>" A sound effect (mp3). --duration. ${cr("sfx")}
|
|
21778
|
+
genex music "<prompt>" A looping instrumental track (mp3). ${cr("music")}
|
|
21779
|
+
--duration 10-300s (default 90).
|
|
21780
|
+
genex voice "<line>" A spoken line (mp3). --voice ${cr("voice")}
|
|
21781
|
+
narrator|heroine|gruff|elder|robot|imp.
|
|
21782
|
+
|
|
21783
|
+
${c.bold("Characters")}
|
|
21784
|
+
genex character "<brief>" Three concept candidates to choose from. ${cr("character_concept")}
|
|
21785
|
+
genex character preview <concept-id> --candidate <1|2|3> --user-approved
|
|
21786
|
+
A 3D preview of the chosen one. ${cr("character_preview")}
|
|
21787
|
+
genex character finalize <preview-id> --user-approved --approve-remesh 10000
|
|
21788
|
+
The rigged, game-ready character. ${cr("character_finalize")}
|
|
21789
|
+
genex character "<brief>" --direct-text
|
|
21790
|
+
One shot, no approval steps. ${cr("character")}
|
|
21791
|
+
genex creature "<desc>" A rigged enemy (biped-shaped bodies). ${cr("character")}
|
|
21792
|
+
genex character animate <id> "<verb>" \u2026
|
|
21793
|
+
Any move, in plain words. --locomotion ${cr("character_motion")} each
|
|
21794
|
+
for the 8-way walk+run set; --video
|
|
21795
|
+
./clip.mp4 to use your own footage.
|
|
21796
|
+
genex character motions <id> List the clips installed on a character.
|
|
21797
|
+
genex animations search "<intent>" Search the ready-made animation library. free
|
|
21798
|
+
|
|
21799
|
+
${c.bold("Workflow")}
|
|
21800
|
+
genex doctor [--json] Sign-in, credits, Node/CLI version, and which
|
|
21801
|
+
lanes are live. Run it when anything fails.
|
|
21802
|
+
genex wait <id> Pick up a generation enqueued with --no-wait.
|
|
21803
|
+
Never bills \u2014 re-running a generate command does.
|
|
21804
|
+
genex wait --all One status line per generation from this folder.
|
|
21805
|
+
genex auth [--force] Connect this machine (or switch accounts).
|
|
21806
|
+
genex tools Re-run setup here (refresh skills + rules).
|
|
21807
|
+
|
|
21808
|
+
${c.bold("Options")}
|
|
21809
|
+
--no-wait Enqueue and return; pick it up with "genex wait <id>".
|
|
21810
|
+
--no-download Print the asset URL only; save nothing.
|
|
21811
|
+
--no-install (tools setup) Don't add the CLI to devDependencies.
|
|
21812
|
+
--out-dir <dir> Where saved assets land (default ./assets).
|
|
21813
|
+
--json Machine-readable output.
|
|
21814
|
+
--quiet Reduce output.
|
|
21815
|
+
--api-url <url> Override the API base URL.
|
|
21816
|
+
--env <path> Token env file (default: ~/.genex/env).
|
|
21817
|
+
-h, --help Show this help. -v, --version Show the version.
|
|
21818
|
+
|
|
21819
|
+
${c.bold("Costs")}
|
|
21820
|
+
Prices above are typical. Live prices and your balance: ${c.cyan("genex doctor")}.
|
|
21821
|
+
A failed generation is refunded automatically. Re-running a generate command
|
|
21822
|
+
starts (and bills) a NEW asset \u2014 attach to a running one with ${c.cyan("genex wait <id>")}.
|
|
21823
|
+
|
|
21824
|
+
${c.bold("Examples")}
|
|
21825
|
+
genex image "weathered treasure map, hand-inked" --transparent
|
|
21826
|
+
genex model "low-poly rusted fuel drum" --no-wait
|
|
21827
|
+
genex texture "mossy cracked cobblestone" --terrain
|
|
21828
|
+
genex character "stylized sci-fi courier, practical clothing" --direct-text
|
|
21829
|
+
genex character animate <character-id> --locomotion --no-wait
|
|
21830
|
+
genex wait --all
|
|
21831
|
+
|
|
21832
|
+
Hosting, publishing, multiplayer, remixing and custom domains are the Genex
|
|
21833
|
+
platform \u2014 build a game there from one prompt: ${c.cyan("https://genex.games")}
|
|
21834
|
+
`;
|
|
20938
21835
|
function parseArgs(argv) {
|
|
20939
21836
|
const parsed = {
|
|
20940
21837
|
command: void 0,
|
|
@@ -21048,6 +21945,12 @@ function parseArgs(argv) {
|
|
|
21048
21945
|
case "--no-wait":
|
|
21049
21946
|
parsed.options.noWait = true;
|
|
21050
21947
|
break;
|
|
21948
|
+
case "--no-download":
|
|
21949
|
+
parsed.options.noDownload = true;
|
|
21950
|
+
break;
|
|
21951
|
+
case "--no-install":
|
|
21952
|
+
parsed.options.noInstall = true;
|
|
21953
|
+
break;
|
|
21051
21954
|
case "--no-controller-pack":
|
|
21052
21955
|
parsed.options.controllerPack = false;
|
|
21053
21956
|
break;
|
|
@@ -21545,13 +22448,17 @@ async function main() {
|
|
|
21545
22448
|
log.plain(getCliVersion());
|
|
21546
22449
|
return;
|
|
21547
22450
|
}
|
|
22451
|
+
const mode = await workspaceMode();
|
|
22452
|
+
setWorkspaceHeader(mode === "tools" ? "tools" : null);
|
|
21548
22453
|
if (parsed.help || parsed.command === void 0) {
|
|
21549
|
-
log.plain(HELP);
|
|
22454
|
+
log.plain(mode === "tools" ? HELP_TOOLS : HELP);
|
|
21550
22455
|
return;
|
|
21551
22456
|
}
|
|
21552
22457
|
const quietUpdates = Boolean(parsed.options.quiet || parsed.options.json);
|
|
21553
22458
|
const updateLog = createLogger({ quiet: quietUpdates });
|
|
21554
|
-
if (parsed.command !== "init"
|
|
22459
|
+
if (parsed.command !== "init" && parsed.command !== "tools") {
|
|
22460
|
+
await syncSkills(updateLog, mode);
|
|
22461
|
+
}
|
|
21555
22462
|
const updateCheck = startUpdateCheck();
|
|
21556
22463
|
Sentry2.setTag("command", parsed.command);
|
|
21557
22464
|
Sentry2.setTag("cli.version", getCliVersion());
|
|
@@ -21565,6 +22472,11 @@ async function main() {
|
|
|
21565
22472
|
ci: !!process.env.CI
|
|
21566
22473
|
});
|
|
21567
22474
|
try {
|
|
22475
|
+
if (mode === "tools" && refusedInTools(parsed.command)) {
|
|
22476
|
+
reportPlatformRefused(log, parsed.command);
|
|
22477
|
+
process.exitCode = 1;
|
|
22478
|
+
return;
|
|
22479
|
+
}
|
|
21568
22480
|
if (parsed.command === "skybox") {
|
|
21569
22481
|
log.error("The generated-sky lane is paused \u2014 `genex skybox` is not available.");
|
|
21570
22482
|
log.plain(
|
|
@@ -21597,6 +22509,16 @@ async function main() {
|
|
|
21597
22509
|
case "init":
|
|
21598
22510
|
await runInit(parsed.options);
|
|
21599
22511
|
break;
|
|
22512
|
+
// AG-911 — the OTHER door into this CLI: mark the folder a Genex Tools
|
|
22513
|
+
// workspace (generation only, assets downloaded locally, nothing created
|
|
22514
|
+
// on the platform). Bare word on purpose: it is the first command a
|
|
22515
|
+
// person runs, from a prompt they pasted.
|
|
22516
|
+
case "tools":
|
|
22517
|
+
await runTools(parsed.options);
|
|
22518
|
+
break;
|
|
22519
|
+
case "doctor":
|
|
22520
|
+
await runDoctor(parsed.options);
|
|
22521
|
+
break;
|
|
21600
22522
|
case "auth":
|
|
21601
22523
|
await runAuth(parsed.options);
|
|
21602
22524
|
break;
|