@genex-ai/cli-demo 1.36.13-dev.1026 → 1.36.14-dev.1027
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -754,7 +754,7 @@ function tokenizeCommand(input) {
|
|
|
754
754
|
// src/utils/logger.ts
|
|
755
755
|
function createLogger(opts = {}) {
|
|
756
756
|
const out = (s) => {
|
|
757
|
-
if (!opts.quiet) writeSafely(process.stdout, s + "\n");
|
|
757
|
+
if (!opts.quiet) writeSafely(opts.toStderr ? process.stderr : process.stdout, s + "\n");
|
|
758
758
|
};
|
|
759
759
|
const err = (s) => {
|
|
760
760
|
writeSafely(process.stderr, s + "\n");
|
|
@@ -1396,6 +1396,7 @@ var REFUSED_IN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1396
1396
|
"cover",
|
|
1397
1397
|
"make-remixable",
|
|
1398
1398
|
"shop",
|
|
1399
|
+
"items",
|
|
1399
1400
|
"domain",
|
|
1400
1401
|
"asset",
|
|
1401
1402
|
"controller"
|
|
@@ -2104,13 +2105,13 @@ async function extractSourceArchive(file, dest, rootFolder) {
|
|
|
2104
2105
|
cursor += 46 + nameLen + extraLen + commentLen;
|
|
2105
2106
|
}
|
|
2106
2107
|
if (cursor !== central.length) badArchive("Source archive directory length mismatch.");
|
|
2107
|
-
const
|
|
2108
|
+
const byName2 = new Map(entries.map((e) => [e.name.normalize("NFC").toLowerCase(), e]));
|
|
2108
2109
|
const sorted = [...entries].sort((a, b) => a.offset - b.offset);
|
|
2109
2110
|
for (let i = 0; i < sorted.length; i++) {
|
|
2110
2111
|
if (i && sorted[i].offset < sorted[i - 1].endOffset) badArchive("Source archive entries overlap.");
|
|
2111
2112
|
const parts = sorted[i].name.split("/");
|
|
2112
2113
|
for (let j = 1; j < parts.length; j++) {
|
|
2113
|
-
const parent =
|
|
2114
|
+
const parent = byName2.get(parts.slice(0, j).join("/").normalize("NFC").toLowerCase());
|
|
2114
2115
|
if (parent && !parent.dir) badArchive("Source archive file/link is used as a directory.");
|
|
2115
2116
|
}
|
|
2116
2117
|
}
|
|
@@ -7232,11 +7233,11 @@ async function childRun(cwd, command, args, timeoutMs = 3e5) {
|
|
|
7232
7233
|
env.YARN_ENABLE_IMMUTABLE_INSTALLS = "false";
|
|
7233
7234
|
const child = spawn4(command, args, { cwd, env, shell: process.platform === "win32" && ["npm", "pnpm", "yarn", "bun"].includes(command), stdio: ["ignore", "pipe", "pipe"] });
|
|
7234
7235
|
let tail = "";
|
|
7235
|
-
const
|
|
7236
|
+
const collect2 = (chunk) => {
|
|
7236
7237
|
tail = (tail + chunk.toString()).slice(-8e3);
|
|
7237
7238
|
};
|
|
7238
|
-
child.stdout.on("data",
|
|
7239
|
-
child.stderr.on("data",
|
|
7239
|
+
child.stdout.on("data", collect2);
|
|
7240
|
+
child.stderr.on("data", collect2);
|
|
7240
7241
|
const timer = setTimeout(() => {
|
|
7241
7242
|
child.kill();
|
|
7242
7243
|
reject(new RemixError("setup_timeout", `${command} timed out. The workspace is retained; retry the same remix command.`));
|
|
@@ -8553,30 +8554,14 @@ async function runDomain(opts) {
|
|
|
8553
8554
|
log.success(`${hostname} disconnected.`);
|
|
8554
8555
|
}
|
|
8555
8556
|
|
|
8556
|
-
// src/
|
|
8557
|
-
|
|
8558
|
-
function money(cents) {
|
|
8559
|
-
return cents === void 0 ? "" : ` ($${(cents / 100).toFixed(2)})`;
|
|
8560
|
-
}
|
|
8561
|
-
async function runShop(opts) {
|
|
8562
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
8563
|
-
const sub = opts.name?.trim() || "list";
|
|
8564
|
-
if (!SUBS.includes(sub)) {
|
|
8565
|
-
log.error(`Unknown subcommand ${c.cyan(sub)}. Use: ${SUBS.join(", ")}.`);
|
|
8566
|
-
process.exitCode = 1;
|
|
8567
|
-
return;
|
|
8568
|
-
}
|
|
8569
|
-
if (opts.type !== void 0 && opts.type !== "consumable" && opts.type !== "durable") {
|
|
8570
|
-
log.error(`Invalid --type value for shop: ${opts.type} (consumable or durable)`);
|
|
8571
|
-
process.exitCode = 1;
|
|
8572
|
-
return;
|
|
8573
|
-
}
|
|
8557
|
+
// src/lib/linked-game.ts
|
|
8558
|
+
async function openLinkedGame(opts, log, rerun) {
|
|
8574
8559
|
const meta = await readProject();
|
|
8575
8560
|
if (!meta?.id) {
|
|
8576
8561
|
log.error("This folder isn't linked to a game.");
|
|
8577
8562
|
log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
|
|
8578
8563
|
process.exitCode = 1;
|
|
8579
|
-
return;
|
|
8564
|
+
return null;
|
|
8580
8565
|
}
|
|
8581
8566
|
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
8582
8567
|
let token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
|
|
@@ -8584,7 +8569,7 @@ async function runShop(opts) {
|
|
|
8584
8569
|
if (opts.noAuth) {
|
|
8585
8570
|
log.error("Not signed in. Re-run without --no-auth to connect.");
|
|
8586
8571
|
process.exitCode = 1;
|
|
8587
|
-
return;
|
|
8572
|
+
return null;
|
|
8588
8573
|
}
|
|
8589
8574
|
log.plain("Not signed in \u2014 connecting\u2026");
|
|
8590
8575
|
try {
|
|
@@ -8597,138 +8582,1389 @@ async function runShop(opts) {
|
|
|
8597
8582
|
} catch (err) {
|
|
8598
8583
|
if (err instanceof AuthPendingError) {
|
|
8599
8584
|
printAuthHandoff(log, err);
|
|
8600
|
-
log.dim(`Then re-run ${c.cyan(
|
|
8601
|
-
return;
|
|
8585
|
+
log.dim(`Then re-run ${c.cyan(rerun)}.`);
|
|
8586
|
+
return null;
|
|
8602
8587
|
}
|
|
8603
8588
|
log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
8604
8589
|
process.exitCode = 1;
|
|
8605
|
-
return;
|
|
8590
|
+
return null;
|
|
8606
8591
|
}
|
|
8607
8592
|
await writeUserToken(token, opts.envPath, apiUrl);
|
|
8608
8593
|
}
|
|
8609
8594
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
8610
8595
|
const call2 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, ...init?.headers ?? {} } });
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8596
|
+
return { meta, apiUrl, call: call2 };
|
|
8597
|
+
}
|
|
8598
|
+
function formatCoins(coins) {
|
|
8599
|
+
return coins.toLocaleString("en-US");
|
|
8600
|
+
}
|
|
8601
|
+
function coinPrice(coins, displayUsdCents) {
|
|
8602
|
+
if (coins === 0) return "Free";
|
|
8603
|
+
if (displayUsdCents === void 0) return `${formatCoins(coins)} coin`;
|
|
8604
|
+
const usd = (displayUsdCents / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
8605
|
+
return `${formatCoins(coins)} coin ($${usd})`;
|
|
8606
|
+
}
|
|
8607
|
+
function priceGridLine(grid, freeAllowed) {
|
|
8608
|
+
return `${[...freeAllowed ? ["Free"] : [], ...grid.map(formatCoins)].join(" \xB7 ")} coin`;
|
|
8609
|
+
}
|
|
8610
|
+
|
|
8611
|
+
// src/lib/item-refusal.ts
|
|
8612
|
+
var ITEM_ERROR_KIND = "genex.item_error";
|
|
8613
|
+
var ITEM_ERROR_VERSION = 1;
|
|
8614
|
+
var ItemErrorCode = {
|
|
8615
|
+
/** A command line the command cannot run (a missing name, a flag in the wrong place). */
|
|
8616
|
+
Usage: "usage",
|
|
8617
|
+
/** A name, description or tag list over its limit, judged here or by the API's validator. */
|
|
8618
|
+
InvalidField: "invalid_field",
|
|
8619
|
+
/** The API refused with no code the CLI can read. */
|
|
8620
|
+
HttpError: "http_error",
|
|
8621
|
+
/** The game folder refused to be written through (a link or a file in the way). */
|
|
8622
|
+
FolderRefused: "folder_refused"
|
|
8623
|
+
};
|
|
8624
|
+
var ITEM_NAME_MAX = 80;
|
|
8625
|
+
var ITEM_DESCRIPTION_MAX = 1e3;
|
|
8626
|
+
var ITEM_TAGS_MAX = 10;
|
|
8627
|
+
var ITEM_TAG_LENGTH_MAX = 32;
|
|
8628
|
+
var FIELD_OF_KEY = {
|
|
8629
|
+
name: "name",
|
|
8630
|
+
description: "description",
|
|
8631
|
+
tags: "tags",
|
|
8632
|
+
priceCoins: "price",
|
|
8633
|
+
iconUrl: "picture",
|
|
8634
|
+
type: "type",
|
|
8635
|
+
shareable: "shared",
|
|
8636
|
+
resellable: "resellable"
|
|
8637
|
+
};
|
|
8638
|
+
var LABEL_OF_KEY = {
|
|
8639
|
+
priceCoins: "price",
|
|
8640
|
+
iconUrl: "picture URL (--icon)"
|
|
8641
|
+
};
|
|
8642
|
+
var formatCount = (n) => n.toLocaleString("en-US");
|
|
8643
|
+
function zodIssuesOf(body) {
|
|
8644
|
+
if (typeof body !== "object" || body === null) return null;
|
|
8645
|
+
const error = body.error;
|
|
8646
|
+
if (typeof error !== "object" || error === null) return null;
|
|
8647
|
+
const issues = error.issues;
|
|
8648
|
+
if (!Array.isArray(issues) || issues.length === 0) return null;
|
|
8649
|
+
return issues.filter((i) => typeof i === "object" && i !== null);
|
|
8650
|
+
}
|
|
8651
|
+
function issueSentence(key, index, issue) {
|
|
8652
|
+
const measured = issue.type ?? issue.origin;
|
|
8653
|
+
const limit = typeof issue.maximum === "number" ? formatCount(issue.maximum) : null;
|
|
8654
|
+
const label = LABEL_OF_KEY[key] ?? key;
|
|
8655
|
+
if (issue.code === "too_big" && limit !== null) {
|
|
8656
|
+
if (key === "tags" && typeof index === "number") return `Each tag is at most ${limit} characters (tag ${index + 1} is longer).`;
|
|
8657
|
+
if (measured === "array") return key === "tags" ? `An item has at most ${limit} tags.` : `The ${label} has at most ${limit} entries.`;
|
|
8658
|
+
if (measured === "string") return `The ${label} is too long: at most ${limit} characters.`;
|
|
8659
|
+
return `The ${label} is at most ${limit}.`;
|
|
8660
|
+
}
|
|
8661
|
+
if (issue.code === "too_small" && issue.minimum === 1 && measured === "string") {
|
|
8662
|
+
return key === "tags" ? "A tag can't be empty." : `The ${label} can't be empty.`;
|
|
8663
|
+
}
|
|
8664
|
+
const detail = typeof issue.message === "string" ? `: ${issue.message}` : ".";
|
|
8665
|
+
return `The ${label} was refused${detail}`;
|
|
8666
|
+
}
|
|
8667
|
+
function refusalFromBody(status2, body, fallback) {
|
|
8668
|
+
const typed = body ?? {};
|
|
8669
|
+
if (typeof typed.error === "string") {
|
|
8670
|
+
return {
|
|
8671
|
+
error: typed.error,
|
|
8672
|
+
message: typeof typed.message === "string" && typed.message.trim() ? typed.message : `${fallback} (${typed.error}).`,
|
|
8673
|
+
...typeof typed.field === "string" ? { field: typed.field } : {},
|
|
8674
|
+
...typeof typed.path === "string" ? { path: typed.path } : {},
|
|
8675
|
+
...typeof typed.retryAfterMs === "number" ? { retryAfterMs: typed.retryAfterMs } : {}
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
const issues = zodIssuesOf(body);
|
|
8679
|
+
if (issues) {
|
|
8680
|
+
const sentences = [];
|
|
8681
|
+
let field;
|
|
8682
|
+
for (const issue of issues) {
|
|
8683
|
+
const [key, index] = Array.isArray(issue.path) ? issue.path : [];
|
|
8684
|
+
if (typeof key !== "string") continue;
|
|
8685
|
+
field ??= FIELD_OF_KEY[key] ?? key;
|
|
8686
|
+
const sentence = issueSentence(key, index, issue);
|
|
8687
|
+
if (!sentences.includes(sentence)) sentences.push(sentence);
|
|
8688
|
+
}
|
|
8689
|
+
if (sentences.length > 0) {
|
|
8690
|
+
return { error: ItemErrorCode.InvalidField, message: sentences.join(" "), ...field ? { field } : {} };
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
return { error: ItemErrorCode.HttpError, message: `${fallback} (HTTP ${status2}).` };
|
|
8694
|
+
}
|
|
8695
|
+
async function bodyOf(res) {
|
|
8696
|
+
return res.json().catch(() => null);
|
|
8697
|
+
}
|
|
8698
|
+
function storedTags(tags2) {
|
|
8699
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8700
|
+
for (const tag of tags2) {
|
|
8701
|
+
const stored = tag.trim().replace(/\s+/g, " ").toLowerCase();
|
|
8702
|
+
if (stored) seen.add(stored);
|
|
8703
|
+
}
|
|
8704
|
+
return [...seen];
|
|
8705
|
+
}
|
|
8706
|
+
function checkItemWords(words2) {
|
|
8707
|
+
const invalid2 = (field, message) => ({ error: ItemErrorCode.InvalidField, field, message });
|
|
8708
|
+
const name = words2.name?.trim();
|
|
8709
|
+
if (name !== void 0 && name.length > ITEM_NAME_MAX) {
|
|
8710
|
+
return invalid2("name", `The name is too long: at most ${formatCount(ITEM_NAME_MAX)} characters (it has ${formatCount(name.length)}).`);
|
|
8711
|
+
}
|
|
8712
|
+
const description = words2.description?.trim();
|
|
8713
|
+
if (description !== void 0 && description.length > ITEM_DESCRIPTION_MAX) {
|
|
8714
|
+
return invalid2(
|
|
8715
|
+
"description",
|
|
8716
|
+
`The description is too long: at most ${formatCount(ITEM_DESCRIPTION_MAX)} characters (it has ${formatCount(description.length)}).`
|
|
8717
|
+
);
|
|
8718
|
+
}
|
|
8719
|
+
if (!words2.tags) return null;
|
|
8720
|
+
const tags2 = storedTags(words2.tags);
|
|
8721
|
+
if (tags2.length > ITEM_TAGS_MAX) {
|
|
8722
|
+
return invalid2("tags", `An item has at most ${formatCount(ITEM_TAGS_MAX)} tags (this one has ${formatCount(tags2.length)}).`);
|
|
8723
|
+
}
|
|
8724
|
+
const long = tags2.find((tag) => tag.length > ITEM_TAG_LENGTH_MAX);
|
|
8725
|
+
if (long !== void 0) {
|
|
8726
|
+
return invalid2("tags", `Each tag is at most ${formatCount(ITEM_TAG_LENGTH_MAX)} characters ("${long.slice(0, 40)}${long.length > 40 ? "\u2026" : ""}" has ${formatCount(long.length)}).`);
|
|
8727
|
+
}
|
|
8728
|
+
return null;
|
|
8729
|
+
}
|
|
8730
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
8731
|
+
function itemErrorDocument(refusal2) {
|
|
8732
|
+
const plain = (text) => text.replace(ANSI, "").trim();
|
|
8733
|
+
const hint = refusal2.hints?.map(plain).filter(Boolean).join("\n");
|
|
8734
|
+
return {
|
|
8735
|
+
kind: ITEM_ERROR_KIND,
|
|
8736
|
+
version: ITEM_ERROR_VERSION,
|
|
8737
|
+
error: refusal2.error,
|
|
8738
|
+
...refusal2.field !== void 0 ? { field: refusal2.field } : {},
|
|
8739
|
+
...refusal2.path !== void 0 ? { path: refusal2.path } : {},
|
|
8740
|
+
message: plain(refusal2.message),
|
|
8741
|
+
...refusal2.id !== void 0 ? { id: refusal2.id } : {},
|
|
8742
|
+
...refusal2.retryAfterMs !== void 0 ? { retryAfterMs: refusal2.retryAfterMs } : {},
|
|
8743
|
+
...hint ? { hint } : {}
|
|
8744
|
+
};
|
|
8745
|
+
}
|
|
8746
|
+
function reportItemRefusal(log, json, refusal2, printed = false) {
|
|
8747
|
+
process.exitCode = 1;
|
|
8748
|
+
if (!printed) {
|
|
8749
|
+
const code = refusal2.error === ItemErrorCode.Usage ? "" : ` ${c.dim(`[${refusal2.error}]`)}`;
|
|
8750
|
+
log.error(`${refusal2.message}${code}`);
|
|
8751
|
+
for (const line of refusal2.hints ?? []) writeSafely(process.stderr, `${c.dim(` ${line}`)}
|
|
8752
|
+
`);
|
|
8753
|
+
}
|
|
8754
|
+
if (json) writeSafely(process.stdout, `${JSON.stringify(itemErrorDocument(refusal2))}
|
|
8755
|
+
`);
|
|
8756
|
+
}
|
|
8757
|
+
|
|
8758
|
+
// src/lib/item-media.ts
|
|
8759
|
+
import fs24 from "fs/promises";
|
|
8760
|
+
var MIB = 1024 * 1024;
|
|
8761
|
+
var MediaKind = { Picture: "picture", Model: "model" };
|
|
8762
|
+
var MediaContentType = {
|
|
8763
|
+
Png: "image/png",
|
|
8764
|
+
Webp: "image/webp",
|
|
8765
|
+
Glb: "model/gltf-binary",
|
|
8766
|
+
Zip: "application/zip"
|
|
8767
|
+
};
|
|
8768
|
+
var MEDIA_BYTES_MAX = { picture: 2 * MIB, model: 20 * MIB };
|
|
8769
|
+
var MEDIA_FLAG = { picture: "--image", model: "--model" };
|
|
8770
|
+
var ItemMediaError = class extends Error {
|
|
8771
|
+
};
|
|
8772
|
+
var startsWith = (bytes, ascii, at = 0) => bytes.length >= at + ascii.length && bytes.subarray(at, at + ascii.length).toString("latin1") === ascii;
|
|
8773
|
+
function pictureType(bytes) {
|
|
8774
|
+
if (startsWith(bytes, "\x89PNG\r\n\n")) return MediaContentType.Png;
|
|
8775
|
+
if (startsWith(bytes, "RIFF") && startsWith(bytes, "WEBP", 8)) return MediaContentType.Webp;
|
|
8776
|
+
return null;
|
|
8777
|
+
}
|
|
8778
|
+
function modelType(bytes) {
|
|
8779
|
+
return startsWith(bytes, "glTF") ? MediaContentType.Glb : null;
|
|
8780
|
+
}
|
|
8781
|
+
async function readItemMedia(kind, file) {
|
|
8782
|
+
const flag = MEDIA_FLAG[kind];
|
|
8783
|
+
if (/^https?:\/\//i.test(file)) {
|
|
8784
|
+
throw new ItemMediaError(
|
|
8785
|
+
kind === MediaKind.Picture ? `${flag} takes a file on this machine. For a picture that is already online, pass --icon <url>.` : `${flag} takes a .glb file on this machine.`
|
|
8786
|
+
);
|
|
8787
|
+
}
|
|
8788
|
+
let size;
|
|
8789
|
+
try {
|
|
8790
|
+
const stat = await fs24.stat(file);
|
|
8791
|
+
if (!stat.isFile()) throw new Error("not a file");
|
|
8792
|
+
size = stat.size;
|
|
8793
|
+
} catch {
|
|
8794
|
+
throw new ItemMediaError(`${file} isn't a file (${flag}).`);
|
|
8795
|
+
}
|
|
8796
|
+
const max = MEDIA_BYTES_MAX[kind];
|
|
8797
|
+
if (size > max) {
|
|
8798
|
+
throw new ItemMediaError(`${file} is ${Math.ceil(size / MIB * 10) / 10} MB; ${kind === MediaKind.Picture ? "a picture" : "a model"} may be at most ${max / MIB} MB.`);
|
|
8799
|
+
}
|
|
8800
|
+
const bytes = await fs24.readFile(file);
|
|
8801
|
+
const contentType = kind === MediaKind.Picture ? pictureType(bytes) : modelType(bytes);
|
|
8802
|
+
if (!contentType) {
|
|
8803
|
+
throw new ItemMediaError(
|
|
8804
|
+
kind === MediaKind.Picture ? `${file} isn't a PNG or WebP picture. Render the item on a transparent background and save it as PNG.` : `${file} isn't a binary glTF model (.glb).`
|
|
8805
|
+
);
|
|
8806
|
+
}
|
|
8807
|
+
return { kind, file, bytes, contentType };
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8810
|
+
// src/lib/item-package.ts
|
|
8811
|
+
import { createHash } from "crypto";
|
|
8812
|
+
import { constants as fsConstants } from "fs";
|
|
8813
|
+
import fs25 from "fs/promises";
|
|
8814
|
+
import path24 from "path";
|
|
8815
|
+
import zlib from "zlib";
|
|
8816
|
+
var MIB2 = 1024 * 1024;
|
|
8817
|
+
var ITEM_PACKAGE_BYTES_MAX = 50 * MIB2;
|
|
8818
|
+
var ITEM_PACKAGE_FILES_MAX = 2e3;
|
|
8819
|
+
var ITEM_PACKAGE_UNPACKED_BYTES_MAX = 200 * MIB2;
|
|
8820
|
+
var PackSkip = {
|
|
8821
|
+
/** Git metadata, `node_modules`, `.genex/`, and secrets (`.env`, keys, `.npmrc`). */
|
|
8822
|
+
Excluded: "excluded",
|
|
8823
|
+
/** Never followed, wherever it points. */
|
|
8824
|
+
Symlink: "symlink",
|
|
8825
|
+
/** A socket, FIFO or device. */
|
|
8826
|
+
NotAFile: "not_a_file",
|
|
8827
|
+
/** Bigger than a whole package may be. */
|
|
8828
|
+
TooLarge: "too_large",
|
|
8829
|
+
/** A name another system could not write back safely (a backslash, `:`, a reserved name). */
|
|
8830
|
+
UnsafeName: "unsafe_name",
|
|
8831
|
+
/** A top-level `item.json`: `genex items add` writes its own there ({@link ITEM_FILE_NAME}). */
|
|
8832
|
+
Reserved: "reserved",
|
|
8833
|
+
/** A `genex-items/` folder: other creators' items, for integration in this game only (plan 8.1). */
|
|
8834
|
+
SharedItem: "shared_item"
|
|
8835
|
+
};
|
|
8836
|
+
var PackageErrorCode = {
|
|
8837
|
+
DirMissing: "package_dir_missing",
|
|
8838
|
+
Empty: "package_empty",
|
|
8839
|
+
TooLarge: "package_too_large",
|
|
8840
|
+
TooManyFiles: "package_too_many_files",
|
|
8841
|
+
/** The archive is unsafe or malformed; `path` names the entry when there is one. */
|
|
8842
|
+
Invalid: "package_invalid",
|
|
8843
|
+
/** The archive is well formed but is not what the API recorded for it. */
|
|
8844
|
+
Mismatch: "package_mismatch",
|
|
8845
|
+
DestinationExists: "package_destination_exists",
|
|
8846
|
+
/** The signed link could not be read (network, expiry, an HTTP error). */
|
|
8847
|
+
DownloadFailed: "package_download_failed"
|
|
8848
|
+
};
|
|
8849
|
+
var ItemPackageError = class extends Error {
|
|
8850
|
+
code;
|
|
8851
|
+
/** The archive path at fault, when there is one. */
|
|
8852
|
+
entryPath;
|
|
8853
|
+
constructor(code, message, entryPath) {
|
|
8854
|
+
super(message);
|
|
8855
|
+
this.code = code;
|
|
8856
|
+
this.entryPath = entryPath;
|
|
8857
|
+
}
|
|
8858
|
+
};
|
|
8859
|
+
var ITEM_FILE_NAME = "item.json";
|
|
8860
|
+
var SHARED_ITEMS_DIR = "genex-items";
|
|
8861
|
+
function isReservedPath(relPath) {
|
|
8862
|
+
const top = relPath.split("/")[0] ?? "";
|
|
8863
|
+
return top.normalize("NFC").toLowerCase() === ITEM_FILE_NAME;
|
|
8864
|
+
}
|
|
8865
|
+
function packExclusion(relPath, name) {
|
|
8866
|
+
if (excludedSourcePath(relPath)) return PackSkip.Excluded;
|
|
8867
|
+
if (name.toLowerCase() === SHARED_ITEMS_DIR) return PackSkip.SharedItem;
|
|
8868
|
+
if (!relPath.includes("/") && isReservedPath(relPath)) return PackSkip.Reserved;
|
|
8869
|
+
return null;
|
|
8870
|
+
}
|
|
8871
|
+
async function packItemDirectory(dir) {
|
|
8872
|
+
const root = await realDirectory(dir);
|
|
8873
|
+
const collected = { files: [], skipped: [] };
|
|
8874
|
+
await collect(root, root, "", collected);
|
|
8875
|
+
if (collected.files.length === 0) {
|
|
8876
|
+
throw new ItemPackageError(PackageErrorCode.Empty, `${dir} has no files to package.`);
|
|
8877
|
+
}
|
|
8878
|
+
const zip = writeZip(collected.files);
|
|
8879
|
+
if (zip.length > ITEM_PACKAGE_BYTES_MAX) {
|
|
8880
|
+
throw new ItemPackageError(
|
|
8881
|
+
PackageErrorCode.TooLarge,
|
|
8882
|
+
`The package is ${megabytes(zip.length)} zipped; a package may be at most ${megabytes(ITEM_PACKAGE_BYTES_MAX)}.`
|
|
8883
|
+
);
|
|
8884
|
+
}
|
|
8885
|
+
return {
|
|
8886
|
+
zip,
|
|
8887
|
+
files: collected.files.map((f) => ({ path: f.path, bytes: f.data.length })),
|
|
8888
|
+
skipped: collected.skipped
|
|
8889
|
+
};
|
|
8890
|
+
}
|
|
8891
|
+
async function realDirectory(dir) {
|
|
8892
|
+
try {
|
|
8893
|
+
const root = await fs25.realpath(dir);
|
|
8894
|
+
if ((await fs25.stat(root)).isDirectory()) return root;
|
|
8895
|
+
} catch {
|
|
8896
|
+
}
|
|
8897
|
+
throw new ItemPackageError(PackageErrorCode.DirMissing, `${dir} is not a folder.`);
|
|
8898
|
+
}
|
|
8899
|
+
async function insideRoot(root, candidate) {
|
|
8900
|
+
try {
|
|
8901
|
+
const real = await fs25.realpath(candidate);
|
|
8902
|
+
return real === root || real.startsWith(root + path24.sep);
|
|
8903
|
+
} catch {
|
|
8904
|
+
return false;
|
|
8905
|
+
}
|
|
8906
|
+
}
|
|
8907
|
+
function portableName(relPath) {
|
|
8908
|
+
try {
|
|
8909
|
+
validateArchivePath(relPath);
|
|
8910
|
+
return true;
|
|
8911
|
+
} catch (err) {
|
|
8912
|
+
if (err instanceof RemixError) return false;
|
|
8913
|
+
throw err;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
var byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
8917
|
+
async function collect(root, abs, rel, out) {
|
|
8918
|
+
const entries = (await fs25.readdir(abs, { withFileTypes: true })).sort(byName);
|
|
8919
|
+
for (const entry of entries) {
|
|
8920
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
8921
|
+
const full = path24.join(abs, entry.name);
|
|
8922
|
+
const excluded = packExclusion(relPath, entry.name);
|
|
8923
|
+
if (excluded) {
|
|
8924
|
+
out.skipped.push({ path: relPath, reason: excluded });
|
|
8925
|
+
continue;
|
|
8616
8926
|
}
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8927
|
+
if (entry.isSymbolicLink()) {
|
|
8928
|
+
out.skipped.push({ path: relPath, reason: PackSkip.Symlink });
|
|
8929
|
+
continue;
|
|
8930
|
+
}
|
|
8931
|
+
if (entry.isDirectory()) {
|
|
8932
|
+
if (await insideRoot(root, full)) await collect(root, full, relPath, out);
|
|
8933
|
+
else out.skipped.push({ path: relPath, reason: PackSkip.Symlink });
|
|
8934
|
+
continue;
|
|
8935
|
+
}
|
|
8936
|
+
if (!entry.isFile()) {
|
|
8937
|
+
out.skipped.push({ path: relPath, reason: PackSkip.NotAFile });
|
|
8938
|
+
continue;
|
|
8622
8939
|
}
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8940
|
+
if (!portableName(relPath)) {
|
|
8941
|
+
out.skipped.push({ path: relPath, reason: PackSkip.UnsafeName });
|
|
8942
|
+
continue;
|
|
8626
8943
|
}
|
|
8627
|
-
|
|
8628
|
-
|
|
8944
|
+
const read = await readRegularFile(root, full);
|
|
8945
|
+
if (!Buffer.isBuffer(read)) {
|
|
8946
|
+
out.skipped.push({ path: relPath, reason: read });
|
|
8947
|
+
continue;
|
|
8948
|
+
}
|
|
8949
|
+
out.files.push({ path: relPath, data: read });
|
|
8950
|
+
if (out.files.length > ITEM_PACKAGE_FILES_MAX) {
|
|
8951
|
+
throw new ItemPackageError(
|
|
8952
|
+
PackageErrorCode.TooManyFiles,
|
|
8953
|
+
`The folder holds more than ${ITEM_PACKAGE_FILES_MAX} files; a package may hold at most that many.`
|
|
8954
|
+
);
|
|
8629
8955
|
}
|
|
8630
8956
|
}
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8957
|
+
}
|
|
8958
|
+
async function readRegularFile(root, full) {
|
|
8959
|
+
if (!await insideRoot(root, path24.dirname(full))) return PackSkip.Symlink;
|
|
8960
|
+
let handle;
|
|
8961
|
+
try {
|
|
8962
|
+
handle = await fs25.open(full, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
8963
|
+
} catch {
|
|
8964
|
+
return PackSkip.Symlink;
|
|
8965
|
+
}
|
|
8966
|
+
try {
|
|
8967
|
+
const stat = await handle.stat();
|
|
8968
|
+
if (!stat.isFile()) return PackSkip.NotAFile;
|
|
8969
|
+
if (stat.size > ITEM_PACKAGE_BYTES_MAX) return PackSkip.TooLarge;
|
|
8970
|
+
const data = Buffer.alloc(stat.size);
|
|
8971
|
+
let filled = 0;
|
|
8972
|
+
while (filled < data.length) {
|
|
8973
|
+
const { bytesRead } = await handle.read(data, filled, data.length - filled, filled);
|
|
8974
|
+
if (bytesRead === 0) break;
|
|
8975
|
+
filled += bytesRead;
|
|
8976
|
+
}
|
|
8977
|
+
return data.subarray(0, filled);
|
|
8978
|
+
} finally {
|
|
8979
|
+
await handle.close();
|
|
8980
|
+
}
|
|
8981
|
+
}
|
|
8982
|
+
var LOCAL_SIGNATURE = 67324752;
|
|
8983
|
+
var CENTRAL_SIGNATURE = 33639248;
|
|
8984
|
+
var EOCD_SIGNATURE = 101010256;
|
|
8985
|
+
var DESCRIPTOR_SIGNATURE = 134695760;
|
|
8986
|
+
var LOCAL_HEADER_BYTES = 30;
|
|
8987
|
+
var CENTRAL_HEADER_BYTES = 46;
|
|
8988
|
+
var EOCD_BYTES = 22;
|
|
8989
|
+
var COMMENT_MAX = 65535;
|
|
8990
|
+
var METHOD_STORED = 0;
|
|
8991
|
+
var METHOD_DEFLATE = 8;
|
|
8992
|
+
var FLAG_ENCRYPTED = 1;
|
|
8993
|
+
var FLAG_DESCRIPTOR = 8;
|
|
8994
|
+
var FLAG_STRONG_ENCRYPTION = 64;
|
|
8995
|
+
var FLAG_UTF8 = 2048;
|
|
8996
|
+
var FLAG_MASKED_HEADER = 8192;
|
|
8997
|
+
var UNIX_HOST = 3;
|
|
8998
|
+
var MACOS_HOST = 19;
|
|
8999
|
+
var VERSION = 20;
|
|
9000
|
+
var DOS_DATE = 1 << 5 | 1;
|
|
9001
|
+
var S_IFMT = 61440;
|
|
9002
|
+
var S_IFREG = 32768;
|
|
9003
|
+
var S_IFDIR = 16384;
|
|
9004
|
+
var FILE_MODE = 420;
|
|
9005
|
+
function writeZip(files) {
|
|
9006
|
+
const parts = [];
|
|
9007
|
+
const central = [];
|
|
9008
|
+
let offset = 0;
|
|
9009
|
+
for (const file of files) {
|
|
9010
|
+
const name = Buffer.from(file.path, "utf8");
|
|
9011
|
+
const crc = sourceCrc32(file.data);
|
|
9012
|
+
const deflated = zlib.deflateRawSync(file.data, { level: 9 });
|
|
9013
|
+
const stored = deflated.length >= file.data.length;
|
|
9014
|
+
const body = stored ? file.data : deflated;
|
|
9015
|
+
const method = stored ? METHOD_STORED : METHOD_DEFLATE;
|
|
9016
|
+
const local = Buffer.alloc(LOCAL_HEADER_BYTES);
|
|
9017
|
+
local.writeUInt32LE(LOCAL_SIGNATURE, 0);
|
|
9018
|
+
local.writeUInt16LE(VERSION, 4);
|
|
9019
|
+
local.writeUInt16LE(FLAG_UTF8, 6);
|
|
9020
|
+
local.writeUInt16LE(method, 8);
|
|
9021
|
+
local.writeUInt16LE(0, 10);
|
|
9022
|
+
local.writeUInt16LE(DOS_DATE, 12);
|
|
9023
|
+
local.writeUInt32LE(crc, 14);
|
|
9024
|
+
local.writeUInt32LE(body.length, 18);
|
|
9025
|
+
local.writeUInt32LE(file.data.length, 22);
|
|
9026
|
+
local.writeUInt16LE(name.length, 26);
|
|
9027
|
+
local.writeUInt16LE(0, 28);
|
|
9028
|
+
const entry = Buffer.alloc(CENTRAL_HEADER_BYTES);
|
|
9029
|
+
entry.writeUInt32LE(CENTRAL_SIGNATURE, 0);
|
|
9030
|
+
entry.writeUInt16LE(UNIX_HOST << 8 | VERSION, 4);
|
|
9031
|
+
entry.writeUInt16LE(VERSION, 6);
|
|
9032
|
+
entry.writeUInt16LE(FLAG_UTF8, 8);
|
|
9033
|
+
entry.writeUInt16LE(method, 10);
|
|
9034
|
+
entry.writeUInt16LE(0, 12);
|
|
9035
|
+
entry.writeUInt16LE(DOS_DATE, 14);
|
|
9036
|
+
entry.writeUInt32LE(crc, 16);
|
|
9037
|
+
entry.writeUInt32LE(body.length, 20);
|
|
9038
|
+
entry.writeUInt32LE(file.data.length, 24);
|
|
9039
|
+
entry.writeUInt16LE(name.length, 28);
|
|
9040
|
+
entry.writeUInt32LE((S_IFREG | FILE_MODE) << 16 >>> 0, 38);
|
|
9041
|
+
entry.writeUInt32LE(offset, 42);
|
|
9042
|
+
parts.push(local, name, body);
|
|
9043
|
+
central.push(entry, name);
|
|
9044
|
+
offset += local.length + name.length + body.length;
|
|
9045
|
+
}
|
|
9046
|
+
const directory = Buffer.concat(central);
|
|
9047
|
+
const end = Buffer.alloc(EOCD_BYTES);
|
|
9048
|
+
end.writeUInt32LE(EOCD_SIGNATURE, 0);
|
|
9049
|
+
end.writeUInt16LE(files.length, 8);
|
|
9050
|
+
end.writeUInt16LE(files.length, 10);
|
|
9051
|
+
end.writeUInt32LE(directory.length, 12);
|
|
9052
|
+
end.writeUInt32LE(offset, 16);
|
|
9053
|
+
return Buffer.concat([...parts, directory, end]);
|
|
9054
|
+
}
|
|
9055
|
+
function invalid(message, entryPath) {
|
|
9056
|
+
throw new ItemPackageError(PackageErrorCode.Invalid, message, entryPath);
|
|
9057
|
+
}
|
|
9058
|
+
function findDirectory(zip) {
|
|
9059
|
+
for (let at = zip.length - EOCD_BYTES; at >= Math.max(0, zip.length - EOCD_BYTES - COMMENT_MAX); at--) {
|
|
9060
|
+
if (zip.readUInt32LE(at) !== EOCD_SIGNATURE) continue;
|
|
9061
|
+
if (at + EOCD_BYTES + zip.readUInt16LE(at + 20) !== zip.length) continue;
|
|
9062
|
+
const count = zip.readUInt16LE(at + 10);
|
|
9063
|
+
const bytes = zip.readUInt32LE(at + 12);
|
|
9064
|
+
const offset = zip.readUInt32LE(at + 16);
|
|
9065
|
+
const singleDisk = zip.readUInt16LE(at + 4) === 0 && zip.readUInt16LE(at + 6) === 0 && zip.readUInt16LE(at + 8) === count;
|
|
9066
|
+
if (!singleDisk) invalid("The package spans several disks.");
|
|
9067
|
+
if (count === 65535 || bytes === 4294967295 || offset === 4294967295) invalid("The package uses ZIP64, which a package never needs.");
|
|
9068
|
+
if (offset + bytes !== at) invalid("The package's directory is not where its end record says.");
|
|
9069
|
+
return { offset, bytes, count, end: at };
|
|
9070
|
+
}
|
|
9071
|
+
return invalid("The package is not a zip archive.");
|
|
9072
|
+
}
|
|
9073
|
+
function readEntries(zip) {
|
|
9074
|
+
if (zip.length < EOCD_BYTES) invalid("The package is not a zip archive.");
|
|
9075
|
+
if (zip.length > ITEM_PACKAGE_BYTES_MAX) {
|
|
9076
|
+
throw new ItemPackageError(PackageErrorCode.TooLarge, `The package is over ${megabytes(ITEM_PACKAGE_BYTES_MAX)}.`);
|
|
9077
|
+
}
|
|
9078
|
+
const directory = findDirectory(zip);
|
|
9079
|
+
if (directory.count > ITEM_PACKAGE_FILES_MAX * 2) invalid("The package holds too many entries.");
|
|
9080
|
+
const entries = [];
|
|
9081
|
+
const seen = /* @__PURE__ */ new Map();
|
|
9082
|
+
let cursor = directory.offset;
|
|
9083
|
+
let unpacked = 0;
|
|
9084
|
+
for (let i = 0; i < directory.count; i++) {
|
|
9085
|
+
const entry = readEntry(zip, cursor, directory.offset, directory.end);
|
|
9086
|
+
cursor = entry.next;
|
|
9087
|
+
const key = entry.value.path.normalize("NFC").toLowerCase();
|
|
9088
|
+
if (seen.has(key)) invalid(`The package holds ${entry.value.path} twice (names that differ only in case collide on disk).`, entry.value.path);
|
|
9089
|
+
seen.set(key, entry.value.dir);
|
|
9090
|
+
unpacked += entry.value.size;
|
|
9091
|
+
if (unpacked > ITEM_PACKAGE_UNPACKED_BYTES_MAX) invalid(`The package unpacks to more than ${megabytes(ITEM_PACKAGE_UNPACKED_BYTES_MAX)}.`);
|
|
9092
|
+
entries.push(entry.value);
|
|
9093
|
+
}
|
|
9094
|
+
if (cursor !== directory.end) invalid("The package's directory has trailing bytes.");
|
|
9095
|
+
if (entries.filter((e) => !e.dir).length > ITEM_PACKAGE_FILES_MAX) invalid(`The package holds more than ${ITEM_PACKAGE_FILES_MAX} files.`);
|
|
9096
|
+
for (const entry of entries) {
|
|
9097
|
+
const parts = entry.path.normalize("NFC").toLowerCase().split("/");
|
|
9098
|
+
for (let depth = 1; depth < parts.length; depth++) {
|
|
9099
|
+
if (seen.get(parts.slice(0, depth).join("/")) === false) invalid(`The package uses the file ${parts.slice(0, depth).join("/")} as a folder.`, entry.path);
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
9102
|
+
const byOffset = [...entries].sort((a, b) => a.dataOffset - b.dataOffset);
|
|
9103
|
+
for (let i = 1; i < byOffset.length; i++) {
|
|
9104
|
+
const previous = byOffset[i - 1];
|
|
9105
|
+
const current = byOffset[i];
|
|
9106
|
+
if (previous && current && current.dataOffset < previous.dataEnd) invalid("The package's entries overlap.", current.path);
|
|
9107
|
+
}
|
|
9108
|
+
return entries;
|
|
9109
|
+
}
|
|
9110
|
+
function readEntry(zip, at, directoryStart, directoryEnd) {
|
|
9111
|
+
if (at + CENTRAL_HEADER_BYTES > directoryEnd || zip.readUInt32LE(at) !== CENTRAL_SIGNATURE) invalid("The package's directory is damaged.");
|
|
9112
|
+
const madeBy = zip.readUInt16LE(at + 4) >>> 8;
|
|
9113
|
+
const flags = zip.readUInt16LE(at + 8);
|
|
9114
|
+
const method = zip.readUInt16LE(at + 10);
|
|
9115
|
+
const crc = zip.readUInt32LE(at + 16);
|
|
9116
|
+
const compressedSize = zip.readUInt32LE(at + 20);
|
|
9117
|
+
const size = zip.readUInt32LE(at + 24);
|
|
9118
|
+
const nameLength = zip.readUInt16LE(at + 28);
|
|
9119
|
+
const extraLength = zip.readUInt16LE(at + 30);
|
|
9120
|
+
const commentLength = zip.readUInt16LE(at + 32);
|
|
9121
|
+
const external = zip.readUInt32LE(at + 38);
|
|
9122
|
+
const localOffset = zip.readUInt32LE(at + 42);
|
|
9123
|
+
const next = at + CENTRAL_HEADER_BYTES + nameLength + extraLength + commentLength;
|
|
9124
|
+
if (next > directoryEnd) invalid("The package's directory is damaged.");
|
|
9125
|
+
const rawName = zip.subarray(at + CENTRAL_HEADER_BYTES, at + CENTRAL_HEADER_BYTES + nameLength).toString("utf8");
|
|
9126
|
+
let name;
|
|
9127
|
+
try {
|
|
9128
|
+
name = validateArchivePath(rawName);
|
|
9129
|
+
} catch (err) {
|
|
9130
|
+
if (err instanceof RemixError) invalid(`The package holds an unsafe path: ${JSON.stringify(rawName)}.`, rawName);
|
|
9131
|
+
throw err;
|
|
9132
|
+
}
|
|
9133
|
+
if (isReservedPath(name)) invalid(`The package holds its own ${ITEM_FILE_NAME}, which genex writes itself.`, name);
|
|
9134
|
+
if (flags & (FLAG_ENCRYPTED | FLAG_STRONG_ENCRYPTION | FLAG_MASKED_HEADER)) invalid(`${name} is encrypted.`, name);
|
|
9135
|
+
if (method !== METHOD_STORED && method !== METHOD_DEFLATE) invalid(`${name} uses a compression a package never needs.`, name);
|
|
9136
|
+
const mode = madeBy === UNIX_HOST || madeBy === MACOS_HOST ? external >>> 16 & S_IFMT : 0;
|
|
9137
|
+
const dir = rawName.endsWith("/") || mode === S_IFDIR;
|
|
9138
|
+
if (mode !== 0 && mode !== S_IFREG && mode !== S_IFDIR) invalid(`${name} is a link or a special file, not a plain file.`, name);
|
|
9139
|
+
if (dir && size !== 0) invalid(`The folder ${name} carries data.`, name);
|
|
9140
|
+
if (size > ITEM_PACKAGE_BYTES_MAX) invalid(`${name} is too large.`, name);
|
|
9141
|
+
if (method === METHOD_STORED && compressedSize !== size) invalid(`${name} has inconsistent sizes.`, name);
|
|
9142
|
+
if (localOffset + LOCAL_HEADER_BYTES > directoryStart || zip.readUInt32LE(localOffset) !== LOCAL_SIGNATURE) invalid(`${name} has no local header.`, name);
|
|
9143
|
+
const localNameLength = zip.readUInt16LE(localOffset + 26);
|
|
9144
|
+
const localExtraLength = zip.readUInt16LE(localOffset + 28);
|
|
9145
|
+
const localName = zip.subarray(localOffset + LOCAL_HEADER_BYTES, localOffset + LOCAL_HEADER_BYTES + localNameLength).toString("utf8");
|
|
9146
|
+
if (localName !== rawName || zip.readUInt16LE(localOffset + 8) !== method) invalid(`${name}'s local header disagrees with the directory.`, name);
|
|
9147
|
+
const dataOffset = localOffset + LOCAL_HEADER_BYTES + localNameLength + localExtraLength;
|
|
9148
|
+
const dataEnd = dataOffset + compressedSize + (flags & FLAG_DESCRIPTOR ? 16 : 0);
|
|
9149
|
+
if (dataEnd > directoryStart) invalid(`${name} is truncated.`, name);
|
|
9150
|
+
if (flags & FLAG_DESCRIPTOR && zip.readUInt32LE(dataOffset + compressedSize) !== DESCRIPTOR_SIGNATURE) {
|
|
9151
|
+
invalid(`${name}'s data descriptor is damaged.`, name);
|
|
9152
|
+
}
|
|
9153
|
+
return { value: { path: name, dir, method, crc, compressedSize, size, dataOffset, dataEnd }, next };
|
|
9154
|
+
}
|
|
9155
|
+
function entryBytes(zip, entry) {
|
|
9156
|
+
const raw = zip.subarray(entry.dataOffset, entry.dataOffset + entry.compressedSize);
|
|
9157
|
+
let data;
|
|
9158
|
+
if (entry.method === METHOD_STORED) data = raw;
|
|
9159
|
+
else {
|
|
9160
|
+
try {
|
|
9161
|
+
data = zlib.inflateRawSync(raw, { maxOutputLength: entry.size + 1 });
|
|
9162
|
+
} catch {
|
|
9163
|
+
return invalid(`${entry.path} does not decompress to its declared size.`, entry.path);
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
if (data.length !== entry.size) invalid(`${entry.path} does not decompress to its declared size.`, entry.path);
|
|
9167
|
+
if (sourceCrc32(data) !== entry.crc) invalid(`${entry.path} fails its checksum.`, entry.path);
|
|
9168
|
+
return data;
|
|
9169
|
+
}
|
|
9170
|
+
var sha2562 = (data) => createHash("sha256").update(data).digest("hex");
|
|
9171
|
+
function checkAgainstManifest(zip, files, manifest) {
|
|
9172
|
+
const expected = new Map(manifest.files.map((f) => [f.path, f]));
|
|
9173
|
+
if (expected.size !== manifest.files.length) throw new ItemPackageError(PackageErrorCode.Mismatch, "The package's manifest lists a file twice.");
|
|
9174
|
+
for (const file of files) {
|
|
9175
|
+
const want = expected.get(file.path);
|
|
9176
|
+
if (!want) throw new ItemPackageError(PackageErrorCode.Mismatch, `${file.path} is in the package but not in its manifest.`, file.path);
|
|
9177
|
+
if (want.bytes !== file.size || want.sha256.toLowerCase() !== sha2562(entryBytes(zip, file))) {
|
|
9178
|
+
throw new ItemPackageError(PackageErrorCode.Mismatch, `${file.path} is not the file the manifest recorded.`, file.path);
|
|
9179
|
+
}
|
|
9180
|
+
expected.delete(file.path);
|
|
9181
|
+
}
|
|
9182
|
+
const missing = [...expected.keys()];
|
|
9183
|
+
if (missing.length > 0) throw new ItemPackageError(PackageErrorCode.Mismatch, `The package is missing ${missing[0]}, which its manifest lists.`, missing[0]);
|
|
9184
|
+
}
|
|
9185
|
+
async function extractItemPackage(zip, dest, manifest) {
|
|
9186
|
+
const entries = readEntries(zip);
|
|
9187
|
+
const files = entries.filter((e) => !e.dir);
|
|
9188
|
+
for (const file of files) entryBytes(zip, file);
|
|
9189
|
+
if (manifest) checkAgainstManifest(zip, files, manifest);
|
|
9190
|
+
try {
|
|
9191
|
+
await fs25.mkdir(dest);
|
|
9192
|
+
} catch (err) {
|
|
9193
|
+
if (err.code === "EEXIST") {
|
|
9194
|
+
throw new ItemPackageError(PackageErrorCode.DestinationExists, `${dest} already exists.`);
|
|
9195
|
+
}
|
|
9196
|
+
throw err;
|
|
9197
|
+
}
|
|
9198
|
+
for (const entry of entries) {
|
|
9199
|
+
const target = path24.join(dest, ...entry.path.split("/"));
|
|
9200
|
+
if (entry.dir) {
|
|
9201
|
+
await fs25.mkdir(target, { recursive: true });
|
|
9202
|
+
continue;
|
|
9203
|
+
}
|
|
9204
|
+
await fs25.mkdir(path24.dirname(target), { recursive: true });
|
|
9205
|
+
await fs25.writeFile(target, entryBytes(zip, entry), { flag: "wx", mode: FILE_MODE });
|
|
9206
|
+
}
|
|
9207
|
+
return files.map((f) => f.path);
|
|
9208
|
+
}
|
|
9209
|
+
function megabytes(bytes) {
|
|
9210
|
+
return `${Math.round(bytes / MIB2 * 10) / 10} MB`;
|
|
9211
|
+
}
|
|
9212
|
+
|
|
9213
|
+
// src/lib/item-transfer.ts
|
|
9214
|
+
var DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
9215
|
+
var UPLOAD_TIMEOUT_MS = 3e5;
|
|
9216
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
9217
|
+
function isTransferUrl(url) {
|
|
9218
|
+
let parsed;
|
|
9219
|
+
try {
|
|
9220
|
+
parsed = new URL(url);
|
|
9221
|
+
} catch {
|
|
9222
|
+
return false;
|
|
9223
|
+
}
|
|
9224
|
+
if (parsed.username || parsed.password) return false;
|
|
9225
|
+
if (parsed.protocol === "https:") return true;
|
|
9226
|
+
return parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname);
|
|
9227
|
+
}
|
|
9228
|
+
async function putSigned(uploadUrl, bytes, contentType) {
|
|
9229
|
+
if (!isTransferUrl(uploadUrl)) throw new Error("The upload link Genex returned isn't an https URL.");
|
|
9230
|
+
const res = await fetch(uploadUrl, {
|
|
9231
|
+
method: "PUT",
|
|
9232
|
+
headers: { "Content-Type": contentType },
|
|
9233
|
+
body: bytes,
|
|
9234
|
+
redirect: "error",
|
|
9235
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
9236
|
+
});
|
|
9237
|
+
if (!res.ok) throw new Error(`The upload was refused (HTTP ${res.status}).`);
|
|
9238
|
+
}
|
|
9239
|
+
async function downloadItemPackage(url, fetchImpl = fetch) {
|
|
9240
|
+
if (!isTransferUrl(url)) {
|
|
9241
|
+
throw new ItemPackageError(PackageErrorCode.Invalid, "The package link Genex returned isn't an https URL.");
|
|
9242
|
+
}
|
|
9243
|
+
let res;
|
|
9244
|
+
try {
|
|
9245
|
+
res = await fetchImpl(url, { redirect: "error", signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
9246
|
+
} catch (err) {
|
|
9247
|
+
throw new ItemPackageError(PackageErrorCode.DownloadFailed, `The package download failed: ${err instanceof Error ? err.message : String(err)}.`);
|
|
9248
|
+
}
|
|
9249
|
+
if (!res.ok || !res.body) {
|
|
9250
|
+
throw new ItemPackageError(PackageErrorCode.DownloadFailed, `The package download failed (HTTP ${res.status}).`);
|
|
9251
|
+
}
|
|
9252
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
9253
|
+
if (declared > ITEM_PACKAGE_BYTES_MAX) {
|
|
9254
|
+
await res.body.cancel().catch(() => {
|
|
9255
|
+
});
|
|
9256
|
+
throw new ItemPackageError(PackageErrorCode.TooLarge, "The package is over the 50 MB limit.");
|
|
9257
|
+
}
|
|
9258
|
+
const chunks = [];
|
|
9259
|
+
let total = 0;
|
|
9260
|
+
for await (const chunk of res.body) {
|
|
9261
|
+
total += chunk.length;
|
|
9262
|
+
if (total > ITEM_PACKAGE_BYTES_MAX) {
|
|
9263
|
+
throw new ItemPackageError(PackageErrorCode.TooLarge, "The package is over the 50 MB limit.");
|
|
8638
9264
|
}
|
|
9265
|
+
chunks.push(chunk);
|
|
9266
|
+
}
|
|
9267
|
+
return Buffer.concat(chunks);
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9270
|
+
// src/commands/shop.ts
|
|
9271
|
+
var ITEM_DRAFT_KIND = "genex.item_draft";
|
|
9272
|
+
var ITEM_DRAFT_VERSION = 1;
|
|
9273
|
+
var SUBS = ["list", "add", "set", "remove", "test", "publish", "unpublish"];
|
|
9274
|
+
var STATE_LABEL = {
|
|
9275
|
+
draft: "draft \u2014 not on sale yet",
|
|
9276
|
+
off_sale: "off sale",
|
|
9277
|
+
taken_down: "taken off sale after a complaint"
|
|
9278
|
+
};
|
|
9279
|
+
var REFUSED_FILE_CODES = /* @__PURE__ */ new Set([
|
|
9280
|
+
"picture_invalid",
|
|
9281
|
+
"model_invalid",
|
|
9282
|
+
"media_type",
|
|
9283
|
+
"media_too_large",
|
|
9284
|
+
"package_invalid",
|
|
9285
|
+
"package_too_large"
|
|
9286
|
+
]);
|
|
9287
|
+
var ANOTHER_FILE = { picture: "<picture.png>", model: "<file.glb>", package: "<dir>" };
|
|
9288
|
+
function usageRefusal(message) {
|
|
9289
|
+
return { error: ItemErrorCode.Usage, message };
|
|
9290
|
+
}
|
|
9291
|
+
function clearTagsRefusal(sub, opts) {
|
|
9292
|
+
if (!opts.clearTags) return null;
|
|
9293
|
+
if (sub !== "set") return usageRefusal(`--clear-tags empties a listed item's tags: ${c.cyan("genex shop set <id> --clear-tags")}.`);
|
|
9294
|
+
if (opts.itemTags?.length) return usageRefusal("Pass --tag (the new list) or --clear-tags (no tags), not both.");
|
|
9295
|
+
return null;
|
|
9296
|
+
}
|
|
9297
|
+
async function runShop(opts) {
|
|
9298
|
+
const log = createLogger({ quiet: opts.quiet, toStderr: opts.json });
|
|
9299
|
+
const refuse = (refusal2) => reportItemRefusal(log, opts.json, refusal2);
|
|
9300
|
+
const sub = opts.name?.trim() || "list";
|
|
9301
|
+
if (!SUBS.includes(sub)) return refuse(usageRefusal(`Unknown subcommand ${c.cyan(sub)}. Use: ${SUBS.join(", ")}.`));
|
|
9302
|
+
if (opts.type !== void 0 && opts.type !== "consumable" && opts.type !== "durable") {
|
|
9303
|
+
return refuse({ ...usageRefusal(`Invalid --type value for shop: ${opts.type} (consumable or durable)`), field: "type" });
|
|
9304
|
+
}
|
|
9305
|
+
const clearTags = clearTagsRefusal(sub, opts);
|
|
9306
|
+
if (clearTags) return refuse(clearTags);
|
|
9307
|
+
let attachments = { media: [], pack: null };
|
|
9308
|
+
if (sub === "add" || sub === "set") {
|
|
9309
|
+
const words2 = checkItemWords({ name: sub === "add" ? opts.hostname : opts.rename, description: opts.description, tags: opts.itemTags });
|
|
9310
|
+
if (words2) return refuse(words2);
|
|
9311
|
+
const prepared = await prepareAttachments(opts, refuse);
|
|
9312
|
+
if (!prepared) return;
|
|
9313
|
+
attachments = prepared;
|
|
9314
|
+
}
|
|
9315
|
+
const game = await openLinkedGame(opts, log, `genex shop ${sub}`);
|
|
9316
|
+
if (!game) return;
|
|
9317
|
+
const shop = new ShopSession(game, log, opts);
|
|
9318
|
+
if (sub === "list") return shop.list();
|
|
9319
|
+
if (sub === "add") return shop.add(attachments);
|
|
9320
|
+
const skuId = opts.hostname?.trim();
|
|
9321
|
+
if (!skuId) return refuse(usageRefusal(`\`genex shop ${sub}\` needs an item id \u2014 see ${c.cyan("genex shop list")}.`));
|
|
9322
|
+
if (sub === "test") return shop.testGrant(skuId);
|
|
9323
|
+
if (sub === "set") return shop.set(skuId, attachments);
|
|
9324
|
+
if (sub === "publish") return shop.publish(skuId);
|
|
9325
|
+
if (sub === "unpublish") return shop.unpublish(skuId);
|
|
9326
|
+
return shop.remove(skuId);
|
|
9327
|
+
}
|
|
9328
|
+
async function judgedMedia(kind, file) {
|
|
9329
|
+
try {
|
|
9330
|
+
return await readItemMedia(kind, file);
|
|
9331
|
+
} catch (err) {
|
|
9332
|
+
if (!(err instanceof ItemMediaError)) throw err;
|
|
9333
|
+
return { error: kind === MediaKind.Picture ? "picture_invalid" : "model_invalid", field: kind, message: err.message };
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
async function prepareAttachments(opts, refuse) {
|
|
9337
|
+
const refused = (refusal2) => {
|
|
9338
|
+
refuse(refusal2);
|
|
9339
|
+
return null;
|
|
9340
|
+
};
|
|
9341
|
+
if (opts.shared === true && opts.type === "consumable") {
|
|
9342
|
+
return refused({
|
|
9343
|
+
error: "not_durable",
|
|
9344
|
+
field: "type",
|
|
9345
|
+
message: "Only a durable item can be shared \u2014 other games keep it, and a consumable is used up. Use --type durable, or drop --shared."
|
|
9346
|
+
});
|
|
9347
|
+
}
|
|
9348
|
+
if (opts.icon && opts.picture) {
|
|
9349
|
+
return refused(usageRefusal("Pass --icon or --image, not both: --icon takes a picture URL, --image a picture file on this machine."));
|
|
9350
|
+
}
|
|
9351
|
+
const media = [];
|
|
9352
|
+
for (const [kind, file] of [[MediaKind.Picture, opts.picture], [MediaKind.Model, opts.modelFile]]) {
|
|
9353
|
+
if (!file) continue;
|
|
9354
|
+
const judged = await judgedMedia(kind, file);
|
|
9355
|
+
if ("error" in judged) return refused(judged);
|
|
9356
|
+
media.push(judged);
|
|
9357
|
+
}
|
|
9358
|
+
let pack2 = null;
|
|
9359
|
+
if (opts.packageDir) {
|
|
9360
|
+
try {
|
|
9361
|
+
pack2 = await packItemDirectory(opts.packageDir);
|
|
9362
|
+
} catch (err) {
|
|
9363
|
+
if (err instanceof ItemPackageError) return refused({ error: "package_invalid", field: "package", message: `${err.message} (--package)` });
|
|
9364
|
+
throw err;
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
return { media, pack: pack2 };
|
|
9368
|
+
}
|
|
9369
|
+
var SKIP_REASON = {
|
|
9370
|
+
excluded: "machine state or a secret",
|
|
9371
|
+
symlink: "a link \u2014 links are never followed",
|
|
9372
|
+
not_a_file: "not a regular file",
|
|
9373
|
+
too_large: "too large for any package",
|
|
9374
|
+
unsafe_name: "a name other systems cannot write back",
|
|
9375
|
+
reserved: "genex items add writes its own item.json there",
|
|
9376
|
+
shared_item: "other creators' items are for this game only"
|
|
9377
|
+
};
|
|
9378
|
+
var ShopSession = class {
|
|
9379
|
+
game;
|
|
9380
|
+
log;
|
|
9381
|
+
opts;
|
|
9382
|
+
projectSkus;
|
|
9383
|
+
constructor(game, log, opts) {
|
|
9384
|
+
this.game = game;
|
|
9385
|
+
this.log = log;
|
|
9386
|
+
this.opts = opts;
|
|
9387
|
+
this.projectSkus = `${game.apiUrl}/api/coin/projects/${encodeURIComponent(game.meta.id)}/skus`;
|
|
9388
|
+
}
|
|
9389
|
+
skuUrl(skuId) {
|
|
9390
|
+
return `${this.game.apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
|
|
9391
|
+
}
|
|
9392
|
+
post(url, body) {
|
|
9393
|
+
return this.game.call(url, { method: "POST", ...body === void 0 ? {} : { body: JSON.stringify(body) } });
|
|
9394
|
+
}
|
|
9395
|
+
/** stdout, whole: the one document a `--json` run prints. */
|
|
9396
|
+
printJson(value, pretty = true) {
|
|
9397
|
+
writeSafely(process.stdout, `${pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)}
|
|
9398
|
+
`);
|
|
9399
|
+
}
|
|
9400
|
+
/**
|
|
9401
|
+
* The draft document: the item flattened with `kind` and `version`. Only an
|
|
9402
|
+
* API that speaks the store contract (it sends `state`) gets the stamp; an
|
|
9403
|
+
* older one's bare item prints as it always did, so the app can never
|
|
9404
|
+
* mistake it for a draft.
|
|
9405
|
+
*/
|
|
9406
|
+
printDraft(sku) {
|
|
9407
|
+
if (typeof sku.state === "string") this.printJson({ kind: ITEM_DRAFT_KIND, version: ITEM_DRAFT_VERSION, ...sku }, false);
|
|
9408
|
+
else this.printJson(sku);
|
|
9409
|
+
}
|
|
9410
|
+
/** Print a refusal this command decided itself (exit code 1). */
|
|
9411
|
+
refuse(refusal2) {
|
|
9412
|
+
reportItemRefusal(this.log, this.opts.json, refusal2);
|
|
9413
|
+
}
|
|
9414
|
+
/** The server's price grid, or null when it cannot be read. */
|
|
9415
|
+
async priceGrid() {
|
|
9416
|
+
const res = await this.game.call(this.projectSkus).catch(() => null);
|
|
9417
|
+
if (!res?.ok) return null;
|
|
9418
|
+
const body = await res.json().catch(() => null);
|
|
9419
|
+
const grid = body?.priceGrid;
|
|
9420
|
+
return Array.isArray(grid) && grid.every((n) => typeof n === "number") ? grid : null;
|
|
9421
|
+
}
|
|
9422
|
+
/**
|
|
9423
|
+
* Does this server take a price of 0 (Free)? Only while its item store is
|
|
9424
|
+
* on (contract D7); the store routes answer then and 404 otherwise, the
|
|
9425
|
+
* probe the MCP connector's `item_draft` makes too. Unknown reads as no, so
|
|
9426
|
+
* Free is never offered where it would be refused.
|
|
9427
|
+
*/
|
|
9428
|
+
async freeAllowed() {
|
|
9429
|
+
const res = await this.game.call(`${this.game.apiUrl}/api/store/catalog?limit=1`).catch(() => null);
|
|
9430
|
+
return res?.ok === true;
|
|
9431
|
+
}
|
|
9432
|
+
/** The grid line every price hint prints, or null when the grid cannot be read. */
|
|
9433
|
+
async gridLine() {
|
|
9434
|
+
const [grid, free] = await Promise.all([this.priceGrid(), this.freeAllowed()]);
|
|
9435
|
+
return grid ? priceGridLine(grid, free) : null;
|
|
9436
|
+
}
|
|
9437
|
+
/** Report a refused API answer (exit code 1): the person's line and hints, and the `--json` document. */
|
|
9438
|
+
async fail(res, what, skuId) {
|
|
9439
|
+
const printed = printedStructuredError(res);
|
|
9440
|
+
reportItemRefusal(this.log, this.opts.json, await this.refusalFor(res, what, skuId, printed), printed);
|
|
9441
|
+
}
|
|
9442
|
+
/** One place to turn an API refusal into something actionable. */
|
|
9443
|
+
async refusalFor(res, what, skuId, printed) {
|
|
9444
|
+
const refusal2 = refusalFromBody(res.status, await bodyOf(res), `Couldn't ${what}`);
|
|
9445
|
+
if (res.status === 404 && refusal2.error === "not_found") {
|
|
9446
|
+
return { ...refusal2, message: "In-game purchases aren't enabled on this environment yet." };
|
|
9447
|
+
}
|
|
9448
|
+
if (skuId && refusal2.error === "project_not_found") return { ...refusal2, message: "That item is not yours (or does not exist)." };
|
|
9449
|
+
if (printed) return refusal2;
|
|
9450
|
+
return this.withHints(refusal2, skuId ?? "<id>");
|
|
9451
|
+
}
|
|
9452
|
+
/** The next step for each refusal the creator can act on. */
|
|
9453
|
+
async withHints(refusal2, id) {
|
|
9454
|
+
const hint = (...hints) => ({ ...refusal2, hints });
|
|
9455
|
+
const media = refusal2.field === "model" ? { flag: "--model", file: ANOTHER_FILE.model } : { flag: "--image", file: ANOTHER_FILE.picture };
|
|
9456
|
+
switch (refusal2.error) {
|
|
9457
|
+
case "price_off_grid":
|
|
9458
|
+
case "bad_price": {
|
|
9459
|
+
const grid = await this.gridLine();
|
|
9460
|
+
const price = this.opts.price;
|
|
9461
|
+
const named = grid && refusal2.error === "price_off_grid" && price !== void 0;
|
|
9462
|
+
return {
|
|
9463
|
+
...refusal2,
|
|
9464
|
+
...named ? { message: `${formatCoins(price)} coin isn't one of this game's prices.` } : {},
|
|
9465
|
+
hints: [...grid ? [`Prices available: ${grid}`] : [], `See the full list any time: ${c.cyan("genex shop list")}`]
|
|
9466
|
+
};
|
|
9467
|
+
}
|
|
9468
|
+
case "not_resellable_type":
|
|
9469
|
+
return hint(`Add it with ${c.cyan("--type durable")} \u2014 a consumable is used up, so there is nothing left to resell.`);
|
|
9470
|
+
case "not_durable":
|
|
9471
|
+
return hint(`Only a durable item can be shared: ${c.cyan(`genex shop set ${id} --type durable`)} first.`);
|
|
9472
|
+
case "picture_missing":
|
|
9473
|
+
return hint(`Render the item on a transparent background, then: ${c.cyan(`genex shop set ${id} --image <picture.png>`)}`);
|
|
9474
|
+
case "picture_invalid":
|
|
9475
|
+
case "media_type":
|
|
9476
|
+
case "media_too_large":
|
|
9477
|
+
return hint(`A picture is a PNG or WebP up to 2 MB, a model a .glb up to 20 MB: ${c.cyan(`genex shop set ${id} ${media.flag} ${media.file}`)}`);
|
|
9478
|
+
case "model_invalid":
|
|
9479
|
+
return hint(`Replace it with another GLB: ${c.cyan(`genex shop set ${id} --model ${ANOTHER_FILE.model}`)}`);
|
|
9480
|
+
case "package_invalid":
|
|
9481
|
+
return hint(
|
|
9482
|
+
`The package holds ${refusal2.path ?? "a path"} that a package may not (a link, an absolute or a ../ path). Fix the folder, then ${c.cyan(`genex shop set ${id} --package <dir>`)}.`
|
|
9483
|
+
);
|
|
9484
|
+
case "package_too_large":
|
|
9485
|
+
return hint("A package may be at most 50 MB zipped.");
|
|
9486
|
+
case "name_refused":
|
|
9487
|
+
return hint(`Rename it: ${c.cyan(`genex shop set ${id} --rename "<name>"`)}`);
|
|
9488
|
+
case "description_refused":
|
|
9489
|
+
return hint(`Rewrite it: ${c.cyan(`genex shop set ${id} --description "<text>"`)}`);
|
|
9490
|
+
case "taken_down":
|
|
9491
|
+
return hint("An admin took this item off sale after a complaint. It can't go back on sale; its owners keep it.");
|
|
9492
|
+
case "publish_rate_limited":
|
|
9493
|
+
return hint(`Too many items put on sale today. Try again in ${Math.max(1, Math.ceil((refusal2.retryAfterMs ?? 0) / 6e4))} minutes.`);
|
|
9494
|
+
case "too_many_skus":
|
|
9495
|
+
return hint(`Retire an item this game no longer sells: ${c.cyan("genex shop remove <id>")}`);
|
|
9496
|
+
case "type_locked":
|
|
9497
|
+
return hint("Someone already owns it, so its type can no longer change.");
|
|
9498
|
+
default:
|
|
9499
|
+
return refusal2;
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
async list() {
|
|
9503
|
+
const [res, free] = await Promise.all([this.game.call(this.projectSkus), this.opts.json ? false : this.freeAllowed()]);
|
|
9504
|
+
if (!res.ok) return this.fail(res, "read your shop");
|
|
9505
|
+
const body = await res.json();
|
|
9506
|
+
if (this.opts.json) return this.printJson(body);
|
|
9507
|
+
const { log } = this;
|
|
9508
|
+
const prices = `Prices available: ${priceGridLine(body.priceGrid, free)}`;
|
|
8639
9509
|
if (body.items.length === 0) {
|
|
8640
9510
|
log.plain("This game sells nothing yet.");
|
|
8641
9511
|
log.dim(` Add something: ${c.cyan('genex shop add "Iron Key" --price 100')}`);
|
|
8642
|
-
log.dim(`
|
|
9512
|
+
log.dim(` ${prices}`);
|
|
8643
9513
|
return;
|
|
8644
9514
|
}
|
|
8645
9515
|
log.plain(`${body.items.length} item${body.items.length === 1 ? "" : "s"}:`);
|
|
8646
9516
|
for (const s of body.items) {
|
|
8647
|
-
const
|
|
9517
|
+
const label = s.retired ? "retired" : s.state === void 0 ? s.active ? null : "retired" : STATE_LABEL[s.state] ?? null;
|
|
9518
|
+
const state = label ? c.dim(` (${label})`) : "";
|
|
9519
|
+
const shared = s.shareable ? c.dim(" \xB7 other games can use it") : "";
|
|
8648
9520
|
const resale = s.resellable ? c.dim(" \xB7 resellable") : "";
|
|
8649
|
-
log.plain(
|
|
8650
|
-
` ${c.cyan(s.id)} ${s.name} \u2014 ${s.priceCoins} coin${money(s.priceDisplayUsdCents)} ${c.dim(s.type)}${resale}${state}`
|
|
8651
|
-
);
|
|
9521
|
+
log.plain(` ${c.cyan(s.id)} ${s.name} \u2014 ${coinPrice(s.priceCoins, s.priceDisplayUsdCents)} ${c.dim(s.type)}${shared}${resale}${state}`);
|
|
8652
9522
|
}
|
|
8653
|
-
log.dim(
|
|
8654
|
-
return;
|
|
9523
|
+
log.dim(prices);
|
|
8655
9524
|
}
|
|
8656
|
-
|
|
9525
|
+
async add(attachments) {
|
|
9526
|
+
const { log, opts } = this;
|
|
8657
9527
|
const name = opts.hostname?.trim();
|
|
8658
|
-
if (!name) {
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
body: JSON.stringify({
|
|
8671
|
-
name,
|
|
8672
|
-
priceCoins: opts.price,
|
|
8673
|
-
type: opts.type === "durable" ? "durable" : "consumable",
|
|
8674
|
-
...opts.icon ? { iconUrl: opts.icon } : {},
|
|
8675
|
-
...opts.resellable !== void 0 ? { resellable: opts.resellable } : {}
|
|
8676
|
-
})
|
|
9528
|
+
if (!name) return this.refuse(usageRefusal(`\`genex shop add\` needs a name, e.g. ${c.cyan('genex shop add "Iron Key" --price 100')}.`));
|
|
9529
|
+
if (opts.price === void 0) return this.refuse(usageRefusal(`\`genex shop add\` needs ${c.cyan("--price")}, in coin (0 is Free).`));
|
|
9530
|
+
const type = opts.type ?? (opts.shared ? "durable" : "consumable");
|
|
9531
|
+
const res = await this.post(this.projectSkus, {
|
|
9532
|
+
name,
|
|
9533
|
+
priceCoins: opts.price,
|
|
9534
|
+
type,
|
|
9535
|
+
...opts.description ? { description: opts.description } : {},
|
|
9536
|
+
...opts.itemTags?.length ? { tags: opts.itemTags } : {},
|
|
9537
|
+
...opts.icon ? { iconUrl: opts.icon } : {},
|
|
9538
|
+
...opts.shared !== void 0 ? { shareable: opts.shared } : {},
|
|
9539
|
+
...opts.resellable !== void 0 ? { resellable: opts.resellable } : {}
|
|
8677
9540
|
});
|
|
8678
|
-
if (!
|
|
8679
|
-
const
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
9541
|
+
if (!res.ok) return this.fail(res, "add that item");
|
|
9542
|
+
const created = await res.json();
|
|
9543
|
+
const attached = await this.attach(created.id, created, attachments);
|
|
9544
|
+
if (!attached) return;
|
|
9545
|
+
const sku = attached.sku ?? created;
|
|
9546
|
+
if (opts.json) return this.printDraft(sku);
|
|
9547
|
+
const draft = sku.state === "draft";
|
|
9548
|
+
log.success(`Added ${c.cyan(sku.name)} \u2014 ${coinPrice(sku.priceCoins, sku.priceDisplayUsdCents)}${draft ? ", as a draft" : ""}.`);
|
|
8685
9549
|
log.plain(` id: ${c.cyan(sku.id)}`);
|
|
8686
9550
|
log.dim(` Use it in the game: buy({ skuId: "${sku.id}" })`);
|
|
8687
9551
|
if (sku.resellable) log.dim(" Players can resell this to each other on the market.");
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
log.error(`\`genex shop ${sub}\` needs an item id \u2014 see ${c.cyan("genex shop list")}.`);
|
|
8693
|
-
process.exitCode = 1;
|
|
8694
|
-
return;
|
|
9552
|
+
if (draft) {
|
|
9553
|
+
log.dim(" It is not on sale yet: the person puts it on sale in the Genex app (Create \u203A Items \u203A Selling);");
|
|
9554
|
+
log.dim(` from a terminal: ${c.cyan(`genex shop publish ${sku.id}`)}`);
|
|
9555
|
+
}
|
|
8695
9556
|
}
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
const
|
|
8699
|
-
if (
|
|
8700
|
-
|
|
9557
|
+
async set(skuId, attachments) {
|
|
9558
|
+
const { log, opts } = this;
|
|
9559
|
+
const patch = {};
|
|
9560
|
+
if (opts.rename) patch.name = opts.rename;
|
|
9561
|
+
if (opts.price !== void 0) patch.priceCoins = opts.price;
|
|
9562
|
+
if (opts.type !== void 0) patch.type = opts.type;
|
|
9563
|
+
if (opts.description !== void 0) patch.description = opts.description.trim() === "" ? null : opts.description;
|
|
9564
|
+
if (opts.itemTags?.length) patch.tags = opts.itemTags;
|
|
9565
|
+
if (opts.clearTags) patch.tags = [];
|
|
9566
|
+
if (opts.icon) patch.iconUrl = opts.icon;
|
|
9567
|
+
if (opts.shared !== void 0) patch.shareable = opts.shared;
|
|
9568
|
+
if (opts.resellable !== void 0) patch.resellable = opts.resellable;
|
|
9569
|
+
const uploads = attachments.media.length > 0 || attachments.pack !== null;
|
|
9570
|
+
if (Object.keys(patch).length === 0 && !uploads) {
|
|
9571
|
+
const flags = ["--rename", "--price", "--icon", "--image", "--model", "--package", "--description", "--tag", "--clear-tags", "--shared"].map((f) => c.cyan(f));
|
|
9572
|
+
return this.refuse(usageRefusal(`Nothing to change. Pass ${flags.join(", ")} or ${c.cyan("--resellable")}.`));
|
|
9573
|
+
}
|
|
9574
|
+
let sku = null;
|
|
9575
|
+
if (Object.keys(patch).length > 0) {
|
|
9576
|
+
const res = await this.game.call(this.skuUrl(skuId), { method: "PATCH", body: JSON.stringify(patch) });
|
|
9577
|
+
if (!res.ok) return this.fail(res, "update that item", skuId);
|
|
9578
|
+
sku = await res.json();
|
|
9579
|
+
}
|
|
9580
|
+
const attached = await this.attach(skuId, sku, attachments);
|
|
9581
|
+
const finished = attached?.sku;
|
|
9582
|
+
if (!finished) return;
|
|
9583
|
+
if (opts.json) return this.printDraft(finished);
|
|
9584
|
+
log.success(`Updated ${c.cyan(finished.name)} \u2014 ${coinPrice(finished.priceCoins, finished.priceDisplayUsdCents)}.`);
|
|
9585
|
+
if (opts.resellable === false) log.dim(" Listings already on the market stay up.");
|
|
9586
|
+
if (opts.shared === false) log.dim(" Games that already added it keep it; no new game can add it.");
|
|
9587
|
+
}
|
|
9588
|
+
/**
|
|
9589
|
+
* Upload the picture, the model and the package to an item that exists:
|
|
9590
|
+
* upload-url → PUT to the signed URL → commit, each answering the item as it
|
|
9591
|
+
* now stands. Resolves the item after the last commit (`sku` itself when
|
|
9592
|
+
* there was nothing to upload), or null when an upload was refused —
|
|
9593
|
+
* printed, with the item's id and the command that finishes it, because the
|
|
9594
|
+
* item itself already exists.
|
|
9595
|
+
*/
|
|
9596
|
+
async attach(skuId, sku, attachments) {
|
|
9597
|
+
let current = sku;
|
|
9598
|
+
const skuUrl = this.skuUrl(skuId);
|
|
9599
|
+
const parts = [
|
|
9600
|
+
...attachments.media.map((m) => ({ flag: m.kind === MediaKind.Picture ? "--image" : "--model", source: m.file, kind: m.kind, bytes: m.bytes, contentType: m.contentType })),
|
|
9601
|
+
...attachments.pack ? [{ flag: "--package", source: this.opts.packageDir ?? "<dir>", kind: "package", bytes: attachments.pack.zip, contentType: MediaContentType.Zip }] : []
|
|
9602
|
+
];
|
|
9603
|
+
if (attachments.pack) this.reportSkipped(attachments.pack);
|
|
9604
|
+
for (const part of parts) {
|
|
9605
|
+
const unfinished = (refusal2, printed = false) => {
|
|
9606
|
+
const file = REFUSED_FILE_CODES.has(refusal2.error) ? ANOTHER_FILE[part.kind] : part.source;
|
|
9607
|
+
const finish2 = `The item ${skuId} exists, but its ${part.kind} did not upload. Finish it with: genex shop set ${skuId} ${part.flag} ${file}`;
|
|
9608
|
+
reportItemRefusal(this.log, this.opts.json, { ...refusal2, id: skuId, hints: [...refusal2.hints ?? [], finish2] }, printed);
|
|
9609
|
+
if (printed) this.log.error(finish2);
|
|
9610
|
+
return null;
|
|
9611
|
+
};
|
|
9612
|
+
const refused = async (res, what) => {
|
|
9613
|
+
const printed = printedStructuredError(res);
|
|
9614
|
+
return unfinished(await this.refusalFor(res, what, skuId, printed), printed);
|
|
9615
|
+
};
|
|
9616
|
+
const isPackage = part.kind === "package";
|
|
9617
|
+
const base = isPackage ? `${skuUrl}/package` : `${skuUrl}/media`;
|
|
9618
|
+
this.log.dim(`Uploading the ${part.kind} (${part.bytes.length} bytes)\u2026`);
|
|
9619
|
+
const urlRes = await this.post(`${base}/upload-url`, isPackage ? { bytes: part.bytes.length } : { kind: part.kind, bytes: part.bytes.length, contentType: part.contentType });
|
|
9620
|
+
if (!urlRes.ok) return refused(urlRes, `upload the ${part.kind}`);
|
|
9621
|
+
const { uploadUrl, key } = await urlRes.json();
|
|
9622
|
+
try {
|
|
9623
|
+
await putSigned(uploadUrl, part.bytes, part.contentType);
|
|
9624
|
+
} catch (err) {
|
|
9625
|
+
return unfinished({ error: "media_upload_failed", field: part.kind, message: err instanceof Error ? err.message : String(err) });
|
|
9626
|
+
}
|
|
9627
|
+
const commit = await this.post(base, isPackage ? { key } : { kind: part.kind, key });
|
|
9628
|
+
if (!commit.ok) return refused(commit, `attach the ${part.kind}`);
|
|
9629
|
+
current = await commit.json();
|
|
9630
|
+
}
|
|
9631
|
+
return { sku: current };
|
|
9632
|
+
}
|
|
9633
|
+
reportSkipped(pack2) {
|
|
9634
|
+
this.log.dim(`Packaging ${pack2.files.length} file${pack2.files.length === 1 ? "" : "s"}.`);
|
|
9635
|
+
if (pack2.skipped.length === 0) return;
|
|
9636
|
+
this.log.dim("Left out of the package:");
|
|
9637
|
+
for (const s of pack2.skipped) this.log.dim(` ${s.path} (${SKIP_REASON[s.reason]})`);
|
|
9638
|
+
}
|
|
9639
|
+
async testGrant(skuId) {
|
|
9640
|
+
const { log } = this;
|
|
9641
|
+
const res = await this.post(`${this.skuUrl(skuId)}/test-grant`);
|
|
9642
|
+
if (!res.ok) return this.fail(res, "grant yourself a test copy", skuId);
|
|
9643
|
+
const got = await res.json();
|
|
8701
9644
|
log.success(`You now own ${c.cyan(got.name)} in this game.`);
|
|
8702
9645
|
log.dim(" It cost nothing and no sale was recorded \u2014 it is a test copy.");
|
|
8703
9646
|
log.dim(
|
|
8704
9647
|
got.skuType === "durable" ? " Reload the game: it should appear in your inventory." : " Reload the game: it should be delivered and consumed once."
|
|
8705
9648
|
);
|
|
8706
|
-
return;
|
|
8707
9649
|
}
|
|
8708
|
-
|
|
8709
|
-
const
|
|
8710
|
-
if (
|
|
8711
|
-
|
|
8712
|
-
if (opts.
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
9650
|
+
async publish(skuId) {
|
|
9651
|
+
const res = await this.post(`${this.skuUrl(skuId)}/publish`);
|
|
9652
|
+
if (!res.ok) return this.fail(res, "put that item on sale", skuId);
|
|
9653
|
+
const sku = await res.json();
|
|
9654
|
+
if (this.opts.json) return this.printJson(sku);
|
|
9655
|
+
this.log.success(`${c.cyan(sku.name)} is on sale \u2014 ${coinPrice(sku.priceCoins, sku.priceDisplayUsdCents)}.`);
|
|
9656
|
+
this.log.dim(" Players can buy it in this game's store now.");
|
|
9657
|
+
}
|
|
9658
|
+
async unpublish(skuId) {
|
|
9659
|
+
const res = await this.post(`${this.skuUrl(skuId)}/unpublish`);
|
|
9660
|
+
if (!res.ok) return this.fail(res, "take that item off sale", skuId);
|
|
9661
|
+
const sku = await res.json();
|
|
9662
|
+
if (this.opts.json) return this.printJson(sku);
|
|
9663
|
+
this.log.success(`${c.cyan(sku.name)} is off sale.`);
|
|
9664
|
+
this.log.dim(" Players who already own it keep it.");
|
|
9665
|
+
}
|
|
9666
|
+
async remove(skuId) {
|
|
9667
|
+
const res = await this.game.call(this.skuUrl(skuId), { method: "DELETE" });
|
|
9668
|
+
if (!res.ok) return this.fail(res, "remove that item", skuId);
|
|
9669
|
+
this.log.success("Removed from the shop.");
|
|
9670
|
+
this.log.dim(" Players who already bought it keep it.");
|
|
9671
|
+
}
|
|
9672
|
+
};
|
|
9673
|
+
|
|
9674
|
+
// src/commands/items.ts
|
|
9675
|
+
import crypto6 from "crypto";
|
|
9676
|
+
import fs26 from "fs/promises";
|
|
9677
|
+
import path25 from "path";
|
|
9678
|
+
var SUBS2 = ["search", "add", "remove", "list"];
|
|
9679
|
+
var ITEMS_DIR = SHARED_ITEMS_DIR;
|
|
9680
|
+
var REPLACED_DIR = ".genex/replaced-items";
|
|
9681
|
+
var ITEM_FILE = ITEM_FILE_NAME;
|
|
9682
|
+
var ITEM_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
9683
|
+
async function runItems(opts) {
|
|
9684
|
+
const log = createLogger({ quiet: opts.quiet, toStderr: opts.json });
|
|
9685
|
+
const usage = (message, hints) => reportItemRefusal(log, opts.json, { error: ItemErrorCode.Usage, message, ...hints ? { hints } : {} });
|
|
9686
|
+
const sub = opts.name?.trim() || "list";
|
|
9687
|
+
if (!SUBS2.includes(sub)) return usage(`Unknown subcommand ${c.cyan(sub)}. Use: ${SUBS2.join(", ")}.`);
|
|
9688
|
+
const arg = opts.query?.trim() ?? "";
|
|
9689
|
+
if (sub === "search" && !arg && !opts.itemTags?.length) {
|
|
9690
|
+
return usage(`\`genex items search\` needs words or a --tag, e.g. ${c.cyan('genex items search "wings" --tag fire')}.`);
|
|
9691
|
+
}
|
|
9692
|
+
if ((sub === "add" || sub === "remove") && !ITEM_ID.test(arg)) {
|
|
9693
|
+
return usage(arg ? `${JSON.stringify(arg)} isn't an item id.` : `\`genex items ${sub}\` needs an item id.`, [
|
|
9694
|
+
`Find one with ${c.cyan('genex items search "<words>"')} (or ${c.cyan("genex items list")}).`
|
|
9695
|
+
]);
|
|
9696
|
+
}
|
|
9697
|
+
const game = await openLinkedGame(opts, log, `genex items ${sub}`);
|
|
9698
|
+
if (!game) return;
|
|
9699
|
+
const items = new ItemsSession(game, log, opts);
|
|
9700
|
+
if (sub === "search") return items.search(arg);
|
|
9701
|
+
if (sub === "add") return items.add(arg);
|
|
9702
|
+
if (sub === "remove") return items.remove(arg);
|
|
9703
|
+
return items.list();
|
|
9704
|
+
}
|
|
9705
|
+
var ADD_REFUSAL_HINT = {
|
|
9706
|
+
not_shareable: "Its author no longer lets other games add it; games that already did keep it.",
|
|
9707
|
+
home_game: "This is the item's own game: it is already in its store.",
|
|
9708
|
+
item_off_sale: "Only an item on sale can be added.",
|
|
9709
|
+
taken_down: "Only an item on sale can be added."
|
|
9710
|
+
};
|
|
9711
|
+
function sharePercent(bps) {
|
|
9712
|
+
if (typeof bps !== "number" || !Number.isInteger(bps) || bps < 0 || bps > 1e4) return null;
|
|
9713
|
+
return `${Number((bps / 100).toFixed(2))}%`;
|
|
9714
|
+
}
|
|
9715
|
+
var ItemsSession = class _ItemsSession {
|
|
9716
|
+
game;
|
|
9717
|
+
log;
|
|
9718
|
+
opts;
|
|
9719
|
+
allowances;
|
|
9720
|
+
constructor(game, log, opts) {
|
|
9721
|
+
this.game = game;
|
|
9722
|
+
this.log = log;
|
|
9723
|
+
this.opts = opts;
|
|
9724
|
+
this.allowances = `${game.apiUrl}/api/store/projects/${encodeURIComponent(game.meta.id)}/allowances`;
|
|
9725
|
+
}
|
|
9726
|
+
printJson(value) {
|
|
9727
|
+
writeSafely(process.stdout, `${JSON.stringify(value, null, 2)}
|
|
9728
|
+
`);
|
|
9729
|
+
}
|
|
9730
|
+
/** One place to turn an API refusal into a sentence (and, under --json, the refusal document). */
|
|
9731
|
+
async fail(res, what) {
|
|
9732
|
+
const printed = printedStructuredError(res);
|
|
9733
|
+
const refusal2 = refusalFromBody(res.status, await bodyOf(res), `Couldn't ${what}`);
|
|
9734
|
+
reportItemRefusal(this.log, this.opts.json, _ItemsSession.worded(res.status, refusal2), printed);
|
|
9735
|
+
}
|
|
9736
|
+
/** The refusal in this command's words: the store switched off, and the hints a host can act on. */
|
|
9737
|
+
static worded(status2, refusal2) {
|
|
9738
|
+
if (status2 === 404 && refusal2.error === "not_found") return { ...refusal2, message: "The item store isn't enabled on this environment yet." };
|
|
9739
|
+
const hint = ADD_REFUSAL_HINT[refusal2.error];
|
|
9740
|
+
return hint ? { ...refusal2, hints: [hint] } : refusal2;
|
|
9741
|
+
}
|
|
9742
|
+
/** Print a refusal this command decided itself (exit code 1). */
|
|
9743
|
+
refuse(refusal2) {
|
|
9744
|
+
reportItemRefusal(this.log, this.opts.json, refusal2);
|
|
9745
|
+
}
|
|
9746
|
+
async search(words2) {
|
|
9747
|
+
const qs = new URLSearchParams();
|
|
9748
|
+
if (words2) qs.set("q", words2);
|
|
9749
|
+
if (this.opts.itemTags?.length) qs.set("tags", this.opts.itemTags.join(","));
|
|
9750
|
+
const res = await this.game.call(`${this.game.apiUrl}/api/store/catalog?${qs}`);
|
|
9751
|
+
if (!res.ok) return this.fail(res, "search the items");
|
|
9752
|
+
const body = await res.json();
|
|
9753
|
+
if (this.opts.json) return this.printJson(body);
|
|
9754
|
+
const { log } = this;
|
|
9755
|
+
if (body.items.length === 0) {
|
|
9756
|
+
log.plain("No shared items match.");
|
|
8719
9757
|
return;
|
|
8720
9758
|
}
|
|
8721
|
-
const
|
|
8722
|
-
if (
|
|
8723
|
-
const
|
|
8724
|
-
|
|
8725
|
-
|
|
9759
|
+
const share = sharePercent(body.sellerShareBps);
|
|
9760
|
+
if (share) log.plain(` Your game gets ${share} of every sale of an item it adds; the author and Genex share the rest.`);
|
|
9761
|
+
for (const item of body.items) {
|
|
9762
|
+
const reach = item.gamesCount === 1 ? "in 1 game" : `in ${item.gamesCount} games`;
|
|
9763
|
+
log.plain(` ${c.cyan(item.id)} ${item.name} \u2014 ${coinPrice(item.priceCoins, item.priceDisplayUsdCents)} \xB7 ${reach} \xB7 by ${item.author.name}`);
|
|
9764
|
+
log.dim(` you get ${formatCoins(item.youGetPerSale)} coin per sale${item.tags.length ? ` \xB7 ${item.tags.join(", ")}` : ""}`);
|
|
9765
|
+
if (item.description) log.dim(` ${item.description.length > 120 ? `${item.description.slice(0, 117)}\u2026` : item.description}`);
|
|
9766
|
+
}
|
|
9767
|
+
if (body.nextCursor) log.dim(" More match: narrow the search with more words or a --tag.");
|
|
9768
|
+
log.dim(` Add one to this game: ${c.cyan(`genex items add ${body.items[0]?.id ?? "<id>"}`)} (free; players who own it can use it here)`);
|
|
9769
|
+
}
|
|
9770
|
+
/**
|
|
9771
|
+
* Allow the item here, then write its folder: the package (validated whole)
|
|
9772
|
+
* and item.json. The allowance is server state; when the folder cannot be
|
|
9773
|
+
* written the item stays added and the command says how to fetch it again.
|
|
9774
|
+
*/
|
|
9775
|
+
async add(skuId) {
|
|
9776
|
+
const { log } = this;
|
|
9777
|
+
const added = await this.game.call(this.allowances, { method: "POST", body: JSON.stringify({ skuId }) });
|
|
9778
|
+
if (!added.ok) return this.fail(added, "add that item");
|
|
9779
|
+
const { allowance, package: link, replayed } = await added.json();
|
|
9780
|
+
const item = await this.describe(skuId);
|
|
9781
|
+
if (!item) return;
|
|
9782
|
+
const pkg = link ? await this.packageLink(skuId) : null;
|
|
9783
|
+
if (link && !pkg) return;
|
|
9784
|
+
const itemFile = { ...item, manifest: pkg?.manifest ?? null, addedAt: allowance.addedAt };
|
|
9785
|
+
let files;
|
|
9786
|
+
try {
|
|
9787
|
+
files = await this.writeItemFolder(skuId, itemFile, pkg);
|
|
9788
|
+
} catch (err) {
|
|
9789
|
+
if (err instanceof ItemPackageError) return this.refuse(packageRefusal(skuId, item.name, err));
|
|
9790
|
+
if (err instanceof FolderRefused) return this.refuse(folderRefusal(skuId, item.name, err));
|
|
9791
|
+
throw err;
|
|
9792
|
+
}
|
|
9793
|
+
const dir = `${ITEMS_DIR}/${skuId}`;
|
|
9794
|
+
if (this.opts.json) return this.printJson({ allowance, item: itemFile, dir, files, replayed });
|
|
9795
|
+
const author = item.author ? ` by ${item.author.name}` : "";
|
|
9796
|
+
log.success(`${c.cyan(item.name)}${author} is added to this game \u2014 ${coinPrice(item.priceCoins, item.priceDisplayUsdCents)}.`);
|
|
9797
|
+
log.plain(` ${dir}/${ITEM_FILE}${files.length ? ` and its package (${files.length} file${files.length === 1 ? "" : "s"})` : ""}.`);
|
|
9798
|
+
log.dim(" Another creator's files: use them as reference material, and review any code before you run it.");
|
|
9799
|
+
log.dim(` Next, follow the genex-items skill: decide how it looks and works in this game, wire it to`);
|
|
9800
|
+
log.dim(` buy({ skuId: "${skuId}" }) and apply it from getEntitlements() at boot.`);
|
|
9801
|
+
log.dim(" It shows in this game's store once you release a new build.");
|
|
9802
|
+
log.dim(` Release one with ${c.cyan("genex publish")} (or ${c.cyan("genex preview")}, then ${c.cyan("genex promote")}) once it works in the game.`);
|
|
9803
|
+
}
|
|
9804
|
+
/**
|
|
9805
|
+
* The item's details for item.json. Browse's detail first; the caller's own
|
|
9806
|
+
* item is not browsable (contract D29), so it falls back to this game's
|
|
9807
|
+
* allowances, which carry everything but the description, tags and type.
|
|
9808
|
+
*/
|
|
9809
|
+
async describe(skuId) {
|
|
9810
|
+
const res = await this.game.call(`${this.game.apiUrl}/api/store/catalog/${encodeURIComponent(skuId)}`);
|
|
9811
|
+
if (res.ok) {
|
|
9812
|
+
const { item } = await res.json();
|
|
9813
|
+
return {
|
|
9814
|
+
skuId,
|
|
9815
|
+
name: item.name,
|
|
9816
|
+
description: item.description,
|
|
9817
|
+
type: item.type,
|
|
9818
|
+
tags: item.tags,
|
|
9819
|
+
iconUrl: item.iconUrl,
|
|
9820
|
+
modelUrl: item.modelUrl,
|
|
9821
|
+
priceCoins: item.priceCoins,
|
|
9822
|
+
priceDisplayUsdCents: item.priceDisplayUsdCents,
|
|
9823
|
+
author: item.author,
|
|
9824
|
+
homeGame: item.homeGame
|
|
9825
|
+
};
|
|
9826
|
+
}
|
|
9827
|
+
if (res.status !== 404) {
|
|
9828
|
+
await this.fail(res, "read that item");
|
|
9829
|
+
return null;
|
|
9830
|
+
}
|
|
9831
|
+
const list = await this.game.call(this.allowances);
|
|
9832
|
+
if (!list.ok) {
|
|
9833
|
+
await this.fail(list, "read this game's items");
|
|
9834
|
+
return null;
|
|
9835
|
+
}
|
|
9836
|
+
const { items } = await list.json();
|
|
9837
|
+
const row = items.find((r) => r.item.id === skuId);
|
|
9838
|
+
if (!row) {
|
|
9839
|
+
this.refuse({ error: "sku_not_found", message: `Couldn't read ${skuId} back from this game's items.` });
|
|
9840
|
+
return null;
|
|
9841
|
+
}
|
|
9842
|
+
return {
|
|
9843
|
+
skuId,
|
|
9844
|
+
name: row.item.name,
|
|
9845
|
+
description: null,
|
|
9846
|
+
// Only a durable can be shared (P3).
|
|
9847
|
+
type: "durable",
|
|
9848
|
+
tags: [],
|
|
9849
|
+
iconUrl: row.item.iconUrl,
|
|
9850
|
+
modelUrl: row.item.modelUrl,
|
|
9851
|
+
priceCoins: row.item.priceCoins,
|
|
9852
|
+
priceDisplayUsdCents: row.item.priceDisplayUsdCents,
|
|
9853
|
+
author: row.item.author,
|
|
9854
|
+
homeGame: row.item.homeGame
|
|
9855
|
+
};
|
|
9856
|
+
}
|
|
9857
|
+
/** The package's signed link and its manifest, read together so the two always describe the same upload. */
|
|
9858
|
+
async packageLink(skuId) {
|
|
9859
|
+
const res = await this.game.call(`${this.game.apiUrl}/api/store/items/${encodeURIComponent(skuId)}/package`);
|
|
9860
|
+
if (!res.ok) {
|
|
9861
|
+
await this.fail(res, "fetch the item's package");
|
|
9862
|
+
return null;
|
|
9863
|
+
}
|
|
9864
|
+
return await res.json();
|
|
9865
|
+
}
|
|
9866
|
+
/**
|
|
9867
|
+
* Write `genex-items/<skuId>/`: download and unpack into a fresh staging
|
|
9868
|
+
* folder beside it, add item.json, then swap it in — moving a previous copy
|
|
9869
|
+
* under `.genex/` first. A refusal anywhere leaves the game folder as it was.
|
|
9870
|
+
*/
|
|
9871
|
+
async writeItemFolder(skuId, itemFile, pkg) {
|
|
9872
|
+
const root = process.cwd();
|
|
9873
|
+
const itemsDir = path25.join(root, ITEMS_DIR);
|
|
9874
|
+
await ensureRealDir(itemsDir, ITEMS_DIR);
|
|
9875
|
+
const final = path25.join(itemsDir, skuId);
|
|
9876
|
+
const existing = await lstatOrNull(final);
|
|
9877
|
+
if (existing && !existing.isDirectory()) {
|
|
9878
|
+
throw new FolderRefused(`${ITEMS_DIR}/${skuId} is a link or a file, not a folder. Move it aside first; nothing was written through it.`);
|
|
9879
|
+
}
|
|
9880
|
+
const keep = existing ? await replacedItemsDir(root) : null;
|
|
9881
|
+
const zip = pkg ? await downloadItemPackage(pkg.url) : null;
|
|
9882
|
+
const staging = path25.join(itemsDir, `.${skuId}.incoming-${crypto6.randomBytes(4).toString("hex")}`);
|
|
9883
|
+
try {
|
|
9884
|
+
const files = zip && pkg ? await extractItemPackage(zip, staging, pkg.manifest) : [];
|
|
9885
|
+
if (!zip) await fs26.mkdir(staging);
|
|
9886
|
+
await fs26.writeFile(path25.join(staging, ITEM_FILE), `${JSON.stringify(itemFile, null, 2)}
|
|
9887
|
+
`, { flag: "wx" });
|
|
9888
|
+
if (keep) await fs26.rename(final, path25.join(keep, `${skuId}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`));
|
|
9889
|
+
await fs26.rename(staging, final);
|
|
9890
|
+
return files;
|
|
9891
|
+
} finally {
|
|
9892
|
+
await fs26.rm(staging, { recursive: true, force: true });
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
9895
|
+
async remove(skuId) {
|
|
9896
|
+
const res = await this.game.call(`${this.allowances}/${encodeURIComponent(skuId)}`, { method: "DELETE" });
|
|
9897
|
+
if (!res.ok) return this.fail(res, "remove that item");
|
|
9898
|
+
const body = await res.json();
|
|
9899
|
+
if (this.opts.json) return this.printJson(body);
|
|
9900
|
+
this.log.success(`Removed ${c.cyan(skuId)} from this game's store.`);
|
|
9901
|
+
this.log.dim(" Players who own it keep it; in this game it shows as Not in this game.");
|
|
9902
|
+
this.log.dim(` ${ITEMS_DIR}/${skuId}/ is still on disk: take its code out of the game, then delete the folder.`);
|
|
9903
|
+
}
|
|
9904
|
+
async list() {
|
|
9905
|
+
const res = await this.game.call(this.allowances);
|
|
9906
|
+
if (!res.ok) return this.fail(res, "read this game's items");
|
|
9907
|
+
const body = await res.json();
|
|
9908
|
+
if (this.opts.json) return this.printJson(body);
|
|
9909
|
+
const { log } = this;
|
|
9910
|
+
if (body.items.length === 0) {
|
|
9911
|
+
log.plain("This game has added no shared items.");
|
|
9912
|
+
log.dim(` Find one: ${c.cyan('genex items search "<words>"')}`);
|
|
9913
|
+
return;
|
|
9914
|
+
}
|
|
9915
|
+
for (const { allowance, item } of body.items) {
|
|
9916
|
+
const author = item.author ? ` \xB7 by ${item.author.name}` : "";
|
|
9917
|
+
const live = allowance.liveFromBuildAt ? "" : c.dim(" \xB7 not live yet: it reaches the store with the next published build");
|
|
9918
|
+
log.plain(` ${c.cyan(item.id)} ${item.name} \u2014 ${coinPrice(item.priceCoins, item.priceDisplayUsdCents)}${author}${live}`);
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
};
|
|
9922
|
+
var FolderRefused = class extends Error {
|
|
9923
|
+
};
|
|
9924
|
+
function packageRefusal(skuId, name, err) {
|
|
9925
|
+
return {
|
|
9926
|
+
error: "package_invalid",
|
|
9927
|
+
field: "package",
|
|
9928
|
+
message: `The package was refused: ${err.message}`,
|
|
9929
|
+
id: skuId,
|
|
9930
|
+
hints: [
|
|
9931
|
+
`${name} is added to this game, but nothing was written to ${ITEMS_DIR}/${skuId}/.`,
|
|
9932
|
+
`The package is its author's to fix, not yours. Try again later: ${c.cyan(`genex items add ${skuId}`)}`,
|
|
9933
|
+
"If it keeps failing, report the item as Doesn't work from its game's store, or pick another item."
|
|
9934
|
+
]
|
|
9935
|
+
};
|
|
9936
|
+
}
|
|
9937
|
+
function folderRefusal(skuId, name, err) {
|
|
9938
|
+
return {
|
|
9939
|
+
error: ItemErrorCode.FolderRefused,
|
|
9940
|
+
message: err.message,
|
|
9941
|
+
id: skuId,
|
|
9942
|
+
hints: [`${name} is added to this game, but nothing was written to ${ITEMS_DIR}/${skuId}/. Fix that, then run: genex items add ${skuId}`]
|
|
9943
|
+
};
|
|
9944
|
+
}
|
|
9945
|
+
async function lstatOrNull(p) {
|
|
9946
|
+
try {
|
|
9947
|
+
return await fs26.lstat(p);
|
|
9948
|
+
} catch (err) {
|
|
9949
|
+
if (err.code === "ENOENT") return null;
|
|
9950
|
+
throw err;
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
async function replacedItemsDir(root) {
|
|
9954
|
+
await fs26.mkdir(path25.join(root, ".genex"), { recursive: true });
|
|
9955
|
+
const keep = path25.join(root, ...REPLACED_DIR.split("/"));
|
|
9956
|
+
await ensureRealDir(keep, REPLACED_DIR);
|
|
9957
|
+
return keep;
|
|
9958
|
+
}
|
|
9959
|
+
async function ensureRealDir(dir, label) {
|
|
9960
|
+
const stat = await lstatOrNull(dir);
|
|
9961
|
+
if (!stat) {
|
|
9962
|
+
await fs26.mkdir(dir);
|
|
8726
9963
|
return;
|
|
8727
9964
|
}
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
log.dim(" Players who already bought it keep it.");
|
|
9965
|
+
if (!stat.isDirectory()) {
|
|
9966
|
+
throw new FolderRefused(`${label} is a link or a file, not a folder. Move it aside first; nothing was written through it.`);
|
|
9967
|
+
}
|
|
8732
9968
|
}
|
|
8733
9969
|
|
|
8734
9970
|
// src/lib/promote.ts
|
|
@@ -8922,6 +10158,8 @@ async function runPublish(opts) {
|
|
|
8922
10158
|
process.exitCode = 1;
|
|
8923
10159
|
return;
|
|
8924
10160
|
}
|
|
10161
|
+
const listedOnDisk = await readProject();
|
|
10162
|
+
await writeProject({ ...listedOnDisk ?? meta, status: "published" });
|
|
8925
10163
|
const listed = await res.json().catch(() => null);
|
|
8926
10164
|
if (opts.regenerateCover && listed?.project?.coverAiEnabled === false) {
|
|
8927
10165
|
log.dim(" --regenerate-cover did nothing: this stand doesn't make AI covers.");
|
|
@@ -9217,8 +10455,8 @@ async function runRollback(opts) {
|
|
|
9217
10455
|
}
|
|
9218
10456
|
|
|
9219
10457
|
// src/commands/cover.ts
|
|
9220
|
-
import
|
|
9221
|
-
import
|
|
10458
|
+
import fs27 from "fs/promises";
|
|
10459
|
+
import path26 from "path";
|
|
9222
10460
|
var WHAT_A_COVER_IS = "one real frame of the game: its signature moment, real gameplay, well lit, no menus, popups or busy HUD, landscape 16:9, no text";
|
|
9223
10461
|
async function runCover(opts) {
|
|
9224
10462
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
@@ -9243,7 +10481,7 @@ async function runCover(opts) {
|
|
|
9243
10481
|
}
|
|
9244
10482
|
let bytes;
|
|
9245
10483
|
try {
|
|
9246
|
-
const stat = await
|
|
10484
|
+
const stat = await fs27.stat(file);
|
|
9247
10485
|
if (!stat.isFile()) throw new Error("not a file");
|
|
9248
10486
|
if (stat.size > COVER_MAX_BYTES) {
|
|
9249
10487
|
return finish(
|
|
@@ -9253,43 +10491,43 @@ async function runCover(opts) {
|
|
|
9253
10491
|
log
|
|
9254
10492
|
);
|
|
9255
10493
|
}
|
|
9256
|
-
bytes = await
|
|
10494
|
+
bytes = await fs27.readFile(file);
|
|
9257
10495
|
} catch {
|
|
9258
10496
|
return finish({ kind: "invalid", message: `Can't read ${file} \u2014 pass the path of a PNG, JPEG or WebP image.` }, file, opts, log);
|
|
9259
10497
|
}
|
|
9260
|
-
log.step(`Uploading ${
|
|
10498
|
+
log.step(`Uploading ${path26.basename(file)} as the cover\u2026`);
|
|
9261
10499
|
const outcome2 = await uploadCover({ apiUrl, token, projectId: meta.id, bytes, label: file });
|
|
9262
10500
|
await rememberNomination(bytes, file, outcome2);
|
|
9263
10501
|
finish(outcome2, file, opts, log);
|
|
9264
10502
|
}
|
|
9265
10503
|
async function rememberNomination(bytes, file, outcome2) {
|
|
9266
10504
|
const scratch = await findScratchCover();
|
|
9267
|
-
if (!scratch ||
|
|
10505
|
+
if (!scratch || path26.resolve(scratch) === path26.resolve(file)) {
|
|
9268
10506
|
await rememberCoverUpload(bytes, file, outcome2);
|
|
9269
10507
|
return;
|
|
9270
10508
|
}
|
|
9271
10509
|
if (outcome2.kind !== "applied" && outcome2.kind !== "outranked") return;
|
|
9272
|
-
const stale = await
|
|
10510
|
+
const stale = await fs27.readFile(scratch).catch(() => null);
|
|
9273
10511
|
if (stale) await rememberCoverUpload(stale, scratch, outcome2);
|
|
9274
10512
|
}
|
|
9275
10513
|
async function sendClip(apiUrl, token, projectId, clip, opts, log) {
|
|
9276
10514
|
const file = clip ?? "";
|
|
9277
|
-
const
|
|
9278
|
-
if (!clip) return
|
|
9279
|
-
if (opts.imagePath?.trim()) return
|
|
9280
|
-
if (!opts.commit?.trim()) return
|
|
10515
|
+
const invalid2 = (message) => finishClip({ kind: "invalid", message }, file, opts, log);
|
|
10516
|
+
if (!clip) return invalid2("--commit goes with --clip <file>: pass the recording to send.");
|
|
10517
|
+
if (opts.imagePath?.trim()) return invalid2("Pass either an image (the cover) or --clip <file> (the cover clip), not both.");
|
|
10518
|
+
if (!opts.commit?.trim()) return invalid2("--clip needs --commit <sha>: the git commit of the build the recording shows.");
|
|
9281
10519
|
let bytes;
|
|
9282
10520
|
try {
|
|
9283
|
-
const stat = await
|
|
10521
|
+
const stat = await fs27.stat(clip);
|
|
9284
10522
|
if (!stat.isFile()) throw new Error("not a file");
|
|
9285
10523
|
if (stat.size > CLIP_MAX_BYTES) {
|
|
9286
|
-
return
|
|
10524
|
+
return invalid2(`${clip} is ${(stat.size / 1024 / 1024).toFixed(1)} MB; a cover clip recording can be at most ${CLIP_MAX_BYTES / 1024 / 1024} MB.`);
|
|
9287
10525
|
}
|
|
9288
|
-
bytes = await
|
|
10526
|
+
bytes = await fs27.readFile(clip);
|
|
9289
10527
|
} catch {
|
|
9290
|
-
return
|
|
10528
|
+
return invalid2(`Can't read ${clip} \u2014 pass the path of an MP4 or WebM recording.`);
|
|
9291
10529
|
}
|
|
9292
|
-
log.step(`Uploading ${
|
|
10530
|
+
log.step(`Uploading ${path26.basename(clip)} as the cover clip\u2026`);
|
|
9293
10531
|
const outcome2 = await uploadClip({ apiUrl, token, projectId, bytes, label: clip, buildCommit: opts.commit });
|
|
9294
10532
|
finishClip(outcome2, clip, opts, log);
|
|
9295
10533
|
}
|
|
@@ -9347,8 +10585,8 @@ async function showCover(apiUrl, token, slug, opts, log) {
|
|
|
9347
10585
|
}
|
|
9348
10586
|
|
|
9349
10587
|
// src/commands/generate.ts
|
|
9350
|
-
import
|
|
9351
|
-
import
|
|
10588
|
+
import fs30 from "fs/promises";
|
|
10589
|
+
import path29 from "path";
|
|
9352
10590
|
import { PNG as PNG4 } from "pngjs";
|
|
9353
10591
|
|
|
9354
10592
|
// src/lib/glass.ts
|
|
@@ -9535,13 +10773,13 @@ function ceilingVerdict(input) {
|
|
|
9535
10773
|
}
|
|
9536
10774
|
|
|
9537
10775
|
// src/lib/generation-admission.ts
|
|
9538
|
-
import
|
|
9539
|
-
import
|
|
9540
|
-
import { createHash, randomUUID } from "crypto";
|
|
10776
|
+
import fs29 from "fs/promises";
|
|
10777
|
+
import path28 from "path";
|
|
10778
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
9541
10779
|
|
|
9542
10780
|
// src/lib/asset-budget.ts
|
|
9543
|
-
import
|
|
9544
|
-
import
|
|
10781
|
+
import fs28 from "fs/promises";
|
|
10782
|
+
import path27 from "path";
|
|
9545
10783
|
var DEFAULT_ASSET_BUDGET_SHARE = 0.5;
|
|
9546
10784
|
var DEFAULT_ASSET_BUDGET_CAP = 500;
|
|
9547
10785
|
function envShare(raw, fallback) {
|
|
@@ -9568,10 +10806,10 @@ function allowanceFor(input) {
|
|
|
9568
10806
|
if (!Number.isFinite(spendable) || spendable <= 0) return 0;
|
|
9569
10807
|
return Math.max(0, Math.min(Math.floor(spendable * config.share), config.cap));
|
|
9570
10808
|
}
|
|
9571
|
-
var storePath = (cwd) =>
|
|
10809
|
+
var storePath = (cwd) => path27.join(cwd, ".genex", "asset-budget.json");
|
|
9572
10810
|
async function readBudgetStore(cwd = process.cwd()) {
|
|
9573
10811
|
try {
|
|
9574
|
-
const raw = await
|
|
10812
|
+
const raw = await fs28.readFile(storePath(cwd), "utf8");
|
|
9575
10813
|
const parsed = JSON.parse(raw);
|
|
9576
10814
|
if (!parsed || parsed.v !== 1 || typeof parsed.allowanceCredits !== "number" || !Number.isFinite(parsed.allowanceCredits) || parsed.setBy !== "default" && parsed.setBy !== "user") {
|
|
9577
10815
|
return null;
|
|
@@ -9590,8 +10828,8 @@ async function readBudgetStore(cwd = process.cwd()) {
|
|
|
9590
10828
|
}
|
|
9591
10829
|
async function writeBudgetStore(cwd, store) {
|
|
9592
10830
|
try {
|
|
9593
|
-
await
|
|
9594
|
-
await
|
|
10831
|
+
await fs28.access(path27.join(cwd, ".genex"));
|
|
10832
|
+
await fs28.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
|
|
9595
10833
|
`, "utf8");
|
|
9596
10834
|
return true;
|
|
9597
10835
|
} catch {
|
|
@@ -9602,7 +10840,7 @@ async function ensureBudgetStore(cwd, input) {
|
|
|
9602
10840
|
const existing = await readBudgetStore(cwd);
|
|
9603
10841
|
if (existing) return existing;
|
|
9604
10842
|
try {
|
|
9605
|
-
await
|
|
10843
|
+
await fs28.stat(storePath(cwd));
|
|
9606
10844
|
throw new Error("Stored asset allowance is unreadable or invalid; reconcile it before paid generation.");
|
|
9607
10845
|
} catch (error) {
|
|
9608
10846
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -9706,13 +10944,13 @@ async function assetBudgetGate(input) {
|
|
|
9706
10944
|
|
|
9707
10945
|
// src/lib/generation-admission.ts
|
|
9708
10946
|
async function withAdmissionLock(cwd, work, waitMs = 3e4) {
|
|
9709
|
-
const lock =
|
|
9710
|
-
await
|
|
10947
|
+
const lock = path28.join(cwd, ".genex", "generation-admission.lock");
|
|
10948
|
+
await fs29.mkdir(path28.dirname(lock), { recursive: true });
|
|
9711
10949
|
const deadline = Date.now() + waitMs;
|
|
9712
10950
|
let file;
|
|
9713
10951
|
while (!file) {
|
|
9714
10952
|
try {
|
|
9715
|
-
file = await
|
|
10953
|
+
file = await fs29.open(lock, "wx");
|
|
9716
10954
|
} catch (error) {
|
|
9717
10955
|
if (error.code !== "EEXIST") throw error;
|
|
9718
10956
|
if (Date.now() >= deadline) throw new Error("Another generation admission holds .genex/generation-admission.lock. If its process died, reconcile pending reservations before removing that lock.");
|
|
@@ -9724,7 +10962,7 @@ async function withAdmissionLock(cwd, work, waitMs = 3e4) {
|
|
|
9724
10962
|
return await work();
|
|
9725
10963
|
} finally {
|
|
9726
10964
|
await file.close();
|
|
9727
|
-
await
|
|
10965
|
+
await fs29.unlink(lock);
|
|
9728
10966
|
}
|
|
9729
10967
|
}
|
|
9730
10968
|
async function enqueueWithAllowance(input) {
|
|
@@ -9734,7 +10972,7 @@ async function enqueueWithAllowance(input) {
|
|
|
9734
10972
|
const createPath = input.createPath ?? "/api/generations";
|
|
9735
10973
|
const generic = createPath === "/api/generations";
|
|
9736
10974
|
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${input.token}` };
|
|
9737
|
-
const fingerprint =
|
|
10975
|
+
const fingerprint = createHash2("sha256").update(JSON.stringify([input.apiUrl, createPath, input.body])).digest("hex");
|
|
9738
10976
|
const rows = await readAdmissionRows(cwd);
|
|
9739
10977
|
const pending2 = /* @__PURE__ */ new Map();
|
|
9740
10978
|
for (const row of rows) {
|
|
@@ -9973,7 +11211,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
9973
11211
|
async function inlineLocalImage(filePath, flag) {
|
|
9974
11212
|
let bytes;
|
|
9975
11213
|
try {
|
|
9976
|
-
bytes = await
|
|
11214
|
+
bytes = await fs30.readFile(filePath);
|
|
9977
11215
|
} catch {
|
|
9978
11216
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
9979
11217
|
}
|
|
@@ -9983,7 +11221,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
9983
11221
|
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.`
|
|
9984
11222
|
};
|
|
9985
11223
|
}
|
|
9986
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
11224
|
+
const mime = IMAGE_MIME_BY_EXT[path29.extname(filePath).toLowerCase()] ?? "image/png";
|
|
9987
11225
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
9988
11226
|
}
|
|
9989
11227
|
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.";
|
|
@@ -10178,7 +11416,7 @@ async function runGenerate(kind, opts) {
|
|
|
10178
11416
|
let typedPrompt = opts.prompt?.trim();
|
|
10179
11417
|
if (!typedPrompt && kind === "model" && opts.imageUrl) {
|
|
10180
11418
|
const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
|
|
10181
|
-
typedPrompt = `from image: ${
|
|
11419
|
+
typedPrompt = `from image: ${path29.basename(ref).slice(0, 120)}`;
|
|
10182
11420
|
}
|
|
10183
11421
|
if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
|
|
10184
11422
|
log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
|
|
@@ -10227,7 +11465,7 @@ async function runGenerate(kind, opts) {
|
|
|
10227
11465
|
return;
|
|
10228
11466
|
}
|
|
10229
11467
|
try {
|
|
10230
|
-
const bytes = await
|
|
11468
|
+
const bytes = await fs30.readFile(opts.inpaintUrl);
|
|
10231
11469
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
10232
11470
|
} catch {
|
|
10233
11471
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -10396,7 +11634,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
10396
11634
|
return;
|
|
10397
11635
|
}
|
|
10398
11636
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
10399
|
-
await
|
|
11637
|
+
await fs30.mkdir(outDir, { recursive: true });
|
|
10400
11638
|
const solved = [];
|
|
10401
11639
|
for (let i = 0; i < files.length; i++) {
|
|
10402
11640
|
const f = files[i];
|
|
@@ -10428,8 +11666,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
10428
11666
|
});
|
|
10429
11667
|
continue;
|
|
10430
11668
|
}
|
|
10431
|
-
const outPath =
|
|
10432
|
-
await
|
|
11669
|
+
const outPath = path29.join(outDir, `glass-${i + 1}.png`);
|
|
11670
|
+
await fs30.writeFile(outPath, PNG4.sync.write(r.png));
|
|
10433
11671
|
solved.push({
|
|
10434
11672
|
path: outPath,
|
|
10435
11673
|
url: f.url,
|
|
@@ -11059,8 +12297,8 @@ function writeJson(value) {
|
|
|
11059
12297
|
}
|
|
11060
12298
|
|
|
11061
12299
|
// src/commands/model-sub.ts
|
|
11062
|
-
import
|
|
11063
|
-
import
|
|
12300
|
+
import fs31 from "fs/promises";
|
|
12301
|
+
import path30 from "path";
|
|
11064
12302
|
var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
|
|
11065
12303
|
function apiErrorMessage(data, fallback) {
|
|
11066
12304
|
if (typeof data !== "object" || data === null) return fallback;
|
|
@@ -11075,29 +12313,29 @@ async function importModelFile(args) {
|
|
|
11075
12313
|
const { log } = args;
|
|
11076
12314
|
const filePath = args.filePath.trim();
|
|
11077
12315
|
if (!/\.glb$/i.test(filePath)) {
|
|
11078
|
-
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${
|
|
12316
|
+
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path30.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
|
|
11079
12317
|
return null;
|
|
11080
12318
|
}
|
|
11081
12319
|
let bytes;
|
|
11082
12320
|
try {
|
|
11083
|
-
bytes = await
|
|
12321
|
+
bytes = await fs31.readFile(filePath);
|
|
11084
12322
|
} catch {
|
|
11085
12323
|
log.error(`Couldn't read ${filePath}.`);
|
|
11086
12324
|
return null;
|
|
11087
12325
|
}
|
|
11088
12326
|
if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
|
|
11089
|
-
log.error(`${
|
|
12327
|
+
log.error(`${path30.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
|
|
11090
12328
|
return null;
|
|
11091
12329
|
}
|
|
11092
12330
|
if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
|
|
11093
|
-
log.error(`${
|
|
12331
|
+
log.error(`${path30.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
|
|
11094
12332
|
return null;
|
|
11095
12333
|
}
|
|
11096
12334
|
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
|
|
11097
12335
|
const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
|
|
11098
12336
|
method: "POST",
|
|
11099
12337
|
headers,
|
|
11100
|
-
body: JSON.stringify({ filename:
|
|
12338
|
+
body: JSON.stringify({ filename: path30.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
|
|
11101
12339
|
});
|
|
11102
12340
|
if (printedStructuredError(minted)) return null;
|
|
11103
12341
|
if (!minted.ok) {
|
|
@@ -11106,7 +12344,7 @@ async function importModelFile(args) {
|
|
|
11106
12344
|
return null;
|
|
11107
12345
|
}
|
|
11108
12346
|
const { id, uploadUrl, url } = await minted.json();
|
|
11109
|
-
log.dim(` uploading ${
|
|
12347
|
+
log.dim(` uploading ${path30.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
11110
12348
|
const put = await fetch(uploadUrl, {
|
|
11111
12349
|
method: "PUT",
|
|
11112
12350
|
headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
|
|
@@ -11254,7 +12492,7 @@ async function runModelAnimate(opts) {
|
|
|
11254
12492
|
}
|
|
11255
12493
|
|
|
11256
12494
|
// src/commands/wait.ts
|
|
11257
|
-
import
|
|
12495
|
+
import path31 from "path";
|
|
11258
12496
|
var MAX_TIMEOUT_SEC = 2e6;
|
|
11259
12497
|
var STILL_RUNNING_EXIT = 2;
|
|
11260
12498
|
var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
@@ -11461,7 +12699,7 @@ async function runWaitAll(opts) {
|
|
|
11461
12699
|
if (v?.status !== "completed" || !v.files?.length) continue;
|
|
11462
12700
|
const local = localDeliveryFor("tools", { ...opts, outDir: opts.outDir ?? e.outDir }, e.prompt || e.kind);
|
|
11463
12701
|
if (!local) continue;
|
|
11464
|
-
const outDir =
|
|
12702
|
+
const outDir = path31.join(path31.relative(process.cwd(), cwd) || ".", local.outDir);
|
|
11465
12703
|
const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
|
|
11466
12704
|
const missing = await undeliveredFiles(v.files, target);
|
|
11467
12705
|
if (missing.length === 0) continue;
|
|
@@ -11573,11 +12811,11 @@ async function toRow(e, v, cwd) {
|
|
|
11573
12811
|
}
|
|
11574
12812
|
|
|
11575
12813
|
// src/commands/controller.ts
|
|
11576
|
-
import
|
|
11577
|
-
import
|
|
12814
|
+
import fs33 from "fs/promises";
|
|
12815
|
+
import path33 from "path";
|
|
11578
12816
|
|
|
11579
12817
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
11580
|
-
import { createHash as
|
|
12818
|
+
import { createHash as createHash3 } from "crypto";
|
|
11581
12819
|
|
|
11582
12820
|
// ../../packages/meshy-animation-catalog/src/generated.ts
|
|
11583
12821
|
var MESHY_ANIMATION_CATALOG = [
|
|
@@ -20295,7 +21533,7 @@ function normalizedPackDefinition(definition) {
|
|
|
20295
21533
|
return { key, version: definition.version, actionIds, bindings };
|
|
20296
21534
|
}
|
|
20297
21535
|
function packFingerprint(definition) {
|
|
20298
|
-
return
|
|
21536
|
+
return createHash3("sha256").update(JSON.stringify(definition)).digest("hex");
|
|
20299
21537
|
}
|
|
20300
21538
|
function createMeshyControllerPackSnapshot(definition) {
|
|
20301
21539
|
const normalized = normalizedPackDefinition(definition);
|
|
@@ -20418,9 +21656,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
20418
21656
|
}
|
|
20419
21657
|
|
|
20420
21658
|
// src/lib/anims.ts
|
|
20421
|
-
import
|
|
20422
|
-
import
|
|
20423
|
-
var ANIMS_DEST =
|
|
21659
|
+
import fs32 from "fs/promises";
|
|
21660
|
+
import path32 from "path";
|
|
21661
|
+
var ANIMS_DEST = path32.join("public", "assets", "anims");
|
|
20424
21662
|
var HIDDEN_TAG = "reference";
|
|
20425
21663
|
async function runAnims(opts) {
|
|
20426
21664
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -20436,7 +21674,7 @@ async function runAnims(opts) {
|
|
|
20436
21674
|
printCatalog(log, manifest, selectors);
|
|
20437
21675
|
return;
|
|
20438
21676
|
}
|
|
20439
|
-
const controllerMarker =
|
|
21677
|
+
const controllerMarker = path32.join(root, "src", "controllers", "character");
|
|
20440
21678
|
if (!await exists3(controllerMarker)) {
|
|
20441
21679
|
log.error(
|
|
20442
21680
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -20445,11 +21683,11 @@ async function runAnims(opts) {
|
|
|
20445
21683
|
process.exitCode = 1;
|
|
20446
21684
|
return;
|
|
20447
21685
|
}
|
|
20448
|
-
const destDir =
|
|
20449
|
-
const gameManifestPath =
|
|
21686
|
+
const destDir = path32.join(root, ANIMS_DEST);
|
|
21687
|
+
const gameManifestPath = path32.join(destDir, "manifest.json");
|
|
20450
21688
|
if (opts.reset) {
|
|
20451
|
-
await
|
|
20452
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
21689
|
+
await fs32.rm(destDir, { recursive: true, force: true });
|
|
21690
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path32.sep)} (--reset)`);
|
|
20453
21691
|
}
|
|
20454
21692
|
if (selectors.length === 0) {
|
|
20455
21693
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -20475,7 +21713,7 @@ async function runAnims(opts) {
|
|
|
20475
21713
|
return;
|
|
20476
21714
|
}
|
|
20477
21715
|
const coreNames = new Set(manifest.core);
|
|
20478
|
-
const
|
|
21716
|
+
const byName2 = /* @__PURE__ */ new Map();
|
|
20479
21717
|
let bundledSkips = 0;
|
|
20480
21718
|
for (const entries of resolved.values()) {
|
|
20481
21719
|
for (const entry of entries) {
|
|
@@ -20483,39 +21721,39 @@ async function runAnims(opts) {
|
|
|
20483
21721
|
bundledSkips++;
|
|
20484
21722
|
continue;
|
|
20485
21723
|
}
|
|
20486
|
-
|
|
21724
|
+
byName2.set(entry.name, entry);
|
|
20487
21725
|
}
|
|
20488
21726
|
}
|
|
20489
|
-
const wanted = [...
|
|
20490
|
-
const cacheDir =
|
|
21727
|
+
const wanted = [...byName2.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
21728
|
+
const cacheDir = path32.join(
|
|
20491
21729
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
20492
21730
|
`${manifest.library}-v${manifest.version}`
|
|
20493
21731
|
);
|
|
20494
|
-
await
|
|
20495
|
-
await
|
|
21732
|
+
await fs32.mkdir(cacheDir, { recursive: true });
|
|
21733
|
+
await fs32.mkdir(destDir, { recursive: true });
|
|
20496
21734
|
const base = getAnimsBase(opts.animsBase);
|
|
20497
21735
|
let installedCount = 0;
|
|
20498
21736
|
let presentCount = 0;
|
|
20499
21737
|
let addedBytes = 0;
|
|
20500
21738
|
const failures = [];
|
|
20501
21739
|
for (const entry of wanted) {
|
|
20502
|
-
const dest =
|
|
21740
|
+
const dest = path32.join(destDir, entry.file);
|
|
20503
21741
|
if (await hasSize(dest, entry.bytes)) {
|
|
20504
21742
|
presentCount++;
|
|
20505
21743
|
continue;
|
|
20506
21744
|
}
|
|
20507
21745
|
try {
|
|
20508
|
-
const cached =
|
|
21746
|
+
const cached = path32.join(cacheDir, entry.file);
|
|
20509
21747
|
if (!await hasSize(cached, entry.bytes)) {
|
|
20510
21748
|
const res = await fetch(base + entry.file);
|
|
20511
21749
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
20512
21750
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
20513
|
-
await
|
|
21751
|
+
await fs32.writeFile(cached, buf);
|
|
20514
21752
|
}
|
|
20515
|
-
await
|
|
21753
|
+
await fs32.copyFile(cached, dest);
|
|
20516
21754
|
installedCount++;
|
|
20517
21755
|
addedBytes += entry.bytes;
|
|
20518
|
-
log.dim(` ${
|
|
21756
|
+
log.dim(` ${path32.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
20519
21757
|
} catch (err) {
|
|
20520
21758
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
20521
21759
|
}
|
|
@@ -20531,13 +21769,13 @@ async function runAnims(opts) {
|
|
|
20531
21769
|
version: manifest.version,
|
|
20532
21770
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
20533
21771
|
};
|
|
20534
|
-
await
|
|
21772
|
+
await fs32.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
20535
21773
|
log.plain("");
|
|
20536
21774
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
20537
21775
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
20538
21776
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
20539
21777
|
log.success(
|
|
20540
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
21778
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path32.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
20541
21779
|
);
|
|
20542
21780
|
for (const [selector, entries] of resolved) {
|
|
20543
21781
|
const names2 = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -20569,12 +21807,12 @@ async function loadManifest(baseOverride) {
|
|
|
20569
21807
|
}
|
|
20570
21808
|
} catch {
|
|
20571
21809
|
}
|
|
20572
|
-
const snapshotPath =
|
|
20573
|
-
const manifest = JSON.parse(await
|
|
21810
|
+
const snapshotPath = path32.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
21811
|
+
const manifest = JSON.parse(await fs32.readFile(snapshotPath, "utf8"));
|
|
20574
21812
|
return { manifest, source: "snapshot" };
|
|
20575
21813
|
}
|
|
20576
21814
|
function resolveSelectors(manifest, selectors) {
|
|
20577
|
-
const
|
|
21815
|
+
const byName2 = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
20578
21816
|
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
20579
21817
|
const tags2 = /* @__PURE__ */ new Map();
|
|
20580
21818
|
for (const entry of manifest.clips) {
|
|
@@ -20586,7 +21824,7 @@ function resolveSelectors(manifest, selectors) {
|
|
|
20586
21824
|
}
|
|
20587
21825
|
const out = /* @__PURE__ */ new Map();
|
|
20588
21826
|
for (const selector of selectors) {
|
|
20589
|
-
const exact =
|
|
21827
|
+
const exact = byName2.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
20590
21828
|
if (exact) {
|
|
20591
21829
|
out.set(selector, [exact]);
|
|
20592
21830
|
continue;
|
|
@@ -20596,7 +21834,7 @@ function resolveSelectors(manifest, selectors) {
|
|
|
20596
21834
|
out.set(selector, tagHit);
|
|
20597
21835
|
continue;
|
|
20598
21836
|
}
|
|
20599
|
-
const candidates = [...tags2.keys(), ...
|
|
21837
|
+
const candidates = [...tags2.keys(), ...byName2.keys()];
|
|
20600
21838
|
const close = suggest(selector, candidates);
|
|
20601
21839
|
throw new Error(
|
|
20602
21840
|
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
@@ -20688,21 +21926,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
20688
21926
|
}
|
|
20689
21927
|
async function readGameManifest(file) {
|
|
20690
21928
|
try {
|
|
20691
|
-
return JSON.parse(await
|
|
21929
|
+
return JSON.parse(await fs32.readFile(file, "utf8"));
|
|
20692
21930
|
} catch {
|
|
20693
21931
|
return null;
|
|
20694
21932
|
}
|
|
20695
21933
|
}
|
|
20696
21934
|
async function hasSize(file, bytes) {
|
|
20697
21935
|
try {
|
|
20698
|
-
return (await
|
|
21936
|
+
return (await fs32.stat(file)).size === bytes;
|
|
20699
21937
|
} catch {
|
|
20700
21938
|
return false;
|
|
20701
21939
|
}
|
|
20702
21940
|
}
|
|
20703
21941
|
async function exists3(p) {
|
|
20704
21942
|
try {
|
|
20705
|
-
await
|
|
21943
|
+
await fs32.access(p);
|
|
20706
21944
|
return true;
|
|
20707
21945
|
} catch {
|
|
20708
21946
|
return false;
|
|
@@ -20903,8 +22141,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
20903
22141
|
]
|
|
20904
22142
|
}
|
|
20905
22143
|
};
|
|
20906
|
-
var CODE_DEST =
|
|
20907
|
-
var ASSETS_DEST =
|
|
22144
|
+
var CODE_DEST = path33.join("src", "controllers");
|
|
22145
|
+
var ASSETS_DEST = path33.join("public", "assets");
|
|
20908
22146
|
async function runController(opts) {
|
|
20909
22147
|
const log = createLogger({ quiet: opts.quiet });
|
|
20910
22148
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -20921,31 +22159,31 @@ async function runController(opts) {
|
|
|
20921
22159
|
process.exitCode = 1;
|
|
20922
22160
|
return;
|
|
20923
22161
|
}
|
|
20924
|
-
const srcDir =
|
|
22162
|
+
const srcDir = path33.join(getTemplatesDir(), "controllers");
|
|
20925
22163
|
const root = opts.cwd ?? process.cwd();
|
|
20926
22164
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
20927
22165
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
20928
22166
|
log.plain("");
|
|
20929
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
22167
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path33.sep)}`);
|
|
20930
22168
|
const plan = [
|
|
20931
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
22169
|
+
...set.code.map((rel) => ({ from: rel, rel: path33.join(CODE_DEST, rel) })),
|
|
20932
22170
|
...set.assets.map((rel) => ({
|
|
20933
22171
|
from: rel,
|
|
20934
|
-
rel:
|
|
22172
|
+
rel: path33.join(ASSETS_DEST, path33.basename(rel))
|
|
20935
22173
|
}))
|
|
20936
22174
|
];
|
|
20937
22175
|
let copied = 0;
|
|
20938
22176
|
let skipped = 0;
|
|
20939
22177
|
try {
|
|
20940
22178
|
for (const file of plan) {
|
|
20941
|
-
const dest =
|
|
22179
|
+
const dest = path33.join(root, file.rel);
|
|
20942
22180
|
if (!opts.force && await exists4(dest)) {
|
|
20943
22181
|
skipped++;
|
|
20944
22182
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
20945
22183
|
continue;
|
|
20946
22184
|
}
|
|
20947
|
-
await
|
|
20948
|
-
await
|
|
22185
|
+
await fs33.mkdir(path33.dirname(dest), { recursive: true });
|
|
22186
|
+
await fs33.copyFile(path33.join(srcDir, file.from), dest);
|
|
20949
22187
|
copied++;
|
|
20950
22188
|
log.dim(` ${file.rel}`);
|
|
20951
22189
|
}
|
|
@@ -20998,7 +22236,7 @@ async function runController(opts) {
|
|
|
20998
22236
|
for (const line of set.sketch) {
|
|
20999
22237
|
log.dim(` ${line}`);
|
|
21000
22238
|
}
|
|
21001
|
-
if (kind === "character" && !await exists4(
|
|
22239
|
+
if (kind === "character" && !await exists4(path33.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
21002
22240
|
log.plain("");
|
|
21003
22241
|
log.plain(
|
|
21004
22242
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -21030,9 +22268,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
21030
22268
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
21031
22269
|
}
|
|
21032
22270
|
assertCompleteMeshyControllerPack(manifest);
|
|
21033
|
-
const destination =
|
|
21034
|
-
await
|
|
21035
|
-
await
|
|
22271
|
+
const destination = path33.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
22272
|
+
await fs33.mkdir(path33.dirname(destination), { recursive: true });
|
|
22273
|
+
await fs33.writeFile(
|
|
21036
22274
|
destination,
|
|
21037
22275
|
`${JSON.stringify(manifest, null, 2)}
|
|
21038
22276
|
`
|
|
@@ -21177,14 +22415,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
21177
22415
|
}
|
|
21178
22416
|
async function installFallbackAvatar(args) {
|
|
21179
22417
|
const { root, srcDir, log } = args;
|
|
21180
|
-
const dest =
|
|
21181
|
-
await
|
|
21182
|
-
await
|
|
22418
|
+
const dest = path33.join(root, ASSETS_DEST, "avatar.vrm");
|
|
22419
|
+
await fs33.mkdir(path33.dirname(dest), { recursive: true });
|
|
22420
|
+
await fs33.copyFile(path33.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
21183
22421
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
21184
22422
|
}
|
|
21185
22423
|
async function exists4(p) {
|
|
21186
22424
|
try {
|
|
21187
|
-
await
|
|
22425
|
+
await fs33.access(p);
|
|
21188
22426
|
return true;
|
|
21189
22427
|
} catch {
|
|
21190
22428
|
return false;
|
|
@@ -21192,8 +22430,8 @@ async function exists4(p) {
|
|
|
21192
22430
|
}
|
|
21193
22431
|
|
|
21194
22432
|
// src/commands/character.ts
|
|
21195
|
-
import
|
|
21196
|
-
import
|
|
22433
|
+
import fs34 from "fs/promises";
|
|
22434
|
+
import path34 from "path";
|
|
21197
22435
|
function exactAnimation(selector) {
|
|
21198
22436
|
const trimmed = selector.trim();
|
|
21199
22437
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -21278,7 +22516,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
21278
22516
|
}
|
|
21279
22517
|
process.exitCode = 1;
|
|
21280
22518
|
}
|
|
21281
|
-
var INSTALLED_MANIFEST =
|
|
22519
|
+
var INSTALLED_MANIFEST = path34.join("public", "assets", "meshy-character.json");
|
|
21282
22520
|
async function resolveAdoptTarget(selector) {
|
|
21283
22521
|
const trimmed = selector?.trim();
|
|
21284
22522
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -21287,7 +22525,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
21287
22525
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
21288
22526
|
let raw;
|
|
21289
22527
|
try {
|
|
21290
|
-
raw = await
|
|
22528
|
+
raw = await fs34.readFile(file, "utf8");
|
|
21291
22529
|
} catch {
|
|
21292
22530
|
return {
|
|
21293
22531
|
ok: false,
|
|
@@ -21412,7 +22650,7 @@ async function runCharacterImport(opts) {
|
|
|
21412
22650
|
opts,
|
|
21413
22651
|
ctx,
|
|
21414
22652
|
kind: "character",
|
|
21415
|
-
prompt: `Import ${
|
|
22653
|
+
prompt: `Import ${path34.basename(filePath)} as a rigged character`,
|
|
21416
22654
|
createPath: "/api/characters/import",
|
|
21417
22655
|
body,
|
|
21418
22656
|
quote: price,
|
|
@@ -21957,22 +23195,22 @@ async function context2(opts) {
|
|
|
21957
23195
|
if (!token) return null;
|
|
21958
23196
|
return { token, apiUrl };
|
|
21959
23197
|
}
|
|
21960
|
-
async function readVideo(
|
|
23198
|
+
async function readVideo(path49, log) {
|
|
21961
23199
|
let bytes;
|
|
21962
23200
|
try {
|
|
21963
|
-
bytes = await readFile(
|
|
23201
|
+
bytes = await readFile(path49);
|
|
21964
23202
|
} catch {
|
|
21965
|
-
log.error(`Can't read ${
|
|
23203
|
+
log.error(`Can't read ${path49}.`);
|
|
21966
23204
|
return null;
|
|
21967
23205
|
}
|
|
21968
23206
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
21969
|
-
log.error(`${basename(
|
|
23207
|
+
log.error(`${basename(path49)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
21970
23208
|
return null;
|
|
21971
23209
|
}
|
|
21972
23210
|
return bytes;
|
|
21973
23211
|
}
|
|
21974
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
21975
|
-
const contentType = /\.mov$/i.test(
|
|
23212
|
+
async function uploadVideo(apiUrl, token, characterId, path49, bytes, log) {
|
|
23213
|
+
const contentType = /\.mov$/i.test(path49) ? "video/quicktime" : "video/mp4";
|
|
21976
23214
|
const minted = await apiFetch(
|
|
21977
23215
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
21978
23216
|
{
|
|
@@ -21987,7 +23225,7 @@ async function uploadVideo(apiUrl, token, characterId, path47, bytes, log) {
|
|
|
21987
23225
|
return null;
|
|
21988
23226
|
}
|
|
21989
23227
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
21990
|
-
log.dim(` uploading ${basename(
|
|
23228
|
+
log.dim(` uploading ${basename(path49)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
21991
23229
|
const put = await fetch(uploadUrl, {
|
|
21992
23230
|
method: "PUT",
|
|
21993
23231
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -22319,11 +23557,11 @@ function rank(items, query) {
|
|
|
22319
23557
|
}
|
|
22320
23558
|
|
|
22321
23559
|
// src/commands/motion.ts
|
|
22322
|
-
import
|
|
22323
|
-
import
|
|
23560
|
+
import fs35 from "fs/promises";
|
|
23561
|
+
import path35 from "path";
|
|
22324
23562
|
|
|
22325
23563
|
// src/lib/motion/npz.ts
|
|
22326
|
-
import
|
|
23564
|
+
import zlib2 from "zlib";
|
|
22327
23565
|
function decodeUcs4(b) {
|
|
22328
23566
|
let out = "";
|
|
22329
23567
|
for (let i = 0; i + 4 <= b.length; i += 4) {
|
|
@@ -22374,7 +23612,7 @@ function readNpz(buf) {
|
|
|
22374
23612
|
const lExtraLen = buf.readUInt16LE(lho + 28);
|
|
22375
23613
|
const dataStart = lho + 30 + lNameLen + lExtraLen;
|
|
22376
23614
|
const raw = buf.subarray(dataStart, dataStart + csize);
|
|
22377
|
-
const bytes = method === 8 ?
|
|
23615
|
+
const bytes = method === 8 ? zlib2.inflateRawSync(raw) : Buffer.from(raw);
|
|
22378
23616
|
if (method !== 0 && method !== 8) throw new Error(`unsupported ZIP method ${method} for ${name}`);
|
|
22379
23617
|
out[name.replace(/\.npy$/, "")] = readNpy(bytes);
|
|
22380
23618
|
off += 46 + nameLen + extraLen + cmtLen;
|
|
@@ -23571,7 +24809,7 @@ async function motionGen(opts, log) {
|
|
|
23571
24809
|
}
|
|
23572
24810
|
if (opts.constraintsPath !== void 0) {
|
|
23573
24811
|
try {
|
|
23574
|
-
const raw = await
|
|
24812
|
+
const raw = await fs35.readFile(opts.constraintsPath, "utf8");
|
|
23575
24813
|
generationOptions.constraints = JSON.parse(raw);
|
|
23576
24814
|
} catch {
|
|
23577
24815
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -23595,10 +24833,10 @@ async function motionGen(opts, log) {
|
|
|
23595
24833
|
async function expandTakes(selectors) {
|
|
23596
24834
|
const out = [];
|
|
23597
24835
|
for (const sel of selectors) {
|
|
23598
|
-
const st = await
|
|
24836
|
+
const st = await fs35.stat(sel).catch(() => null);
|
|
23599
24837
|
if (st?.isDirectory()) {
|
|
23600
|
-
const names2 = await
|
|
23601
|
-
for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(
|
|
24838
|
+
const names2 = await fs35.readdir(sel);
|
|
24839
|
+
for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(path35.join(sel, n));
|
|
23602
24840
|
} else if (st?.isFile()) {
|
|
23603
24841
|
out.push(sel);
|
|
23604
24842
|
} else {
|
|
@@ -23633,7 +24871,7 @@ async function motionVerify(opts, log) {
|
|
|
23633
24871
|
let gates = DEFAULT_GATES;
|
|
23634
24872
|
if (opts.gatesPath) {
|
|
23635
24873
|
try {
|
|
23636
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
24874
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs35.readFile(opts.gatesPath, "utf8")));
|
|
23637
24875
|
} catch {
|
|
23638
24876
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
23639
24877
|
process.exitCode = 1;
|
|
@@ -23655,9 +24893,9 @@ async function motionVerify(opts, log) {
|
|
|
23655
24893
|
}
|
|
23656
24894
|
const reports = [];
|
|
23657
24895
|
for (const file of files) {
|
|
23658
|
-
const stem =
|
|
24896
|
+
const stem = path35.basename(file).replace(/\.npz$/, "");
|
|
23659
24897
|
try {
|
|
23660
|
-
reports.push(analyzeTake(stem, await
|
|
24898
|
+
reports.push(analyzeTake(stem, await fs35.readFile(file), gates));
|
|
23661
24899
|
} catch (err) {
|
|
23662
24900
|
reports.push({
|
|
23663
24901
|
take: stem,
|
|
@@ -23695,7 +24933,7 @@ async function motionCompile(opts, log) {
|
|
|
23695
24933
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
23696
24934
|
if (opts.configPath) {
|
|
23697
24935
|
try {
|
|
23698
|
-
const patch = JSON.parse(await
|
|
24936
|
+
const patch = JSON.parse(await fs35.readFile(opts.configPath, "utf8"));
|
|
23699
24937
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
23700
24938
|
} catch {
|
|
23701
24939
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -23713,16 +24951,16 @@ async function motionCompile(opts, log) {
|
|
|
23713
24951
|
}
|
|
23714
24952
|
const inputs = [];
|
|
23715
24953
|
for (const file of files) {
|
|
23716
|
-
const stem =
|
|
24954
|
+
const stem = path35.basename(file).replace(/\.npz$/, "");
|
|
23717
24955
|
try {
|
|
23718
|
-
inputs.push({ stem, take: loadTake(await
|
|
24956
|
+
inputs.push({ stem, take: loadTake(await fs35.readFile(file)) });
|
|
23719
24957
|
} catch (err) {
|
|
23720
24958
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23721
24959
|
process.exitCode = 1;
|
|
23722
24960
|
return;
|
|
23723
24961
|
}
|
|
23724
24962
|
}
|
|
23725
|
-
const setName = opts.set ??
|
|
24963
|
+
const setName = opts.set ?? path35.basename(opts.out).replace(/\.json$/, "");
|
|
23726
24964
|
let result;
|
|
23727
24965
|
try {
|
|
23728
24966
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -23737,9 +24975,9 @@ async function motionCompile(opts, log) {
|
|
|
23737
24975
|
process.exitCode = 1;
|
|
23738
24976
|
return;
|
|
23739
24977
|
}
|
|
23740
|
-
await
|
|
24978
|
+
await fs35.mkdir(path35.dirname(path35.resolve(opts.out)), { recursive: true });
|
|
23741
24979
|
const json = JSON.stringify(result.data);
|
|
23742
|
-
await
|
|
24980
|
+
await fs35.writeFile(opts.out, json);
|
|
23743
24981
|
if (opts.json) {
|
|
23744
24982
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
23745
24983
|
return;
|
|
@@ -23757,9 +24995,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
23757
24995
|
var MOTION_PRESETS = {
|
|
23758
24996
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
23759
24997
|
};
|
|
23760
|
-
var MOTION_DEST =
|
|
24998
|
+
var MOTION_DEST = path35.join("src", "motion");
|
|
23761
24999
|
async function motionInstall(opts, log) {
|
|
23762
|
-
const srcDir =
|
|
25000
|
+
const srcDir = path35.join(getTemplatesDir(), "motion");
|
|
23763
25001
|
const root = opts.cwd ?? process.cwd();
|
|
23764
25002
|
const preset = opts.set;
|
|
23765
25003
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -23770,21 +25008,21 @@ async function motionInstall(opts, log) {
|
|
|
23770
25008
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
23771
25009
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
23772
25010
|
log.plain("");
|
|
23773
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
25011
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path35.sep)}`);
|
|
23774
25012
|
let copied = 0, skipped = 0;
|
|
23775
25013
|
try {
|
|
23776
25014
|
for (const rel of files) {
|
|
23777
|
-
const dest =
|
|
23778
|
-
const exists5 = await
|
|
25015
|
+
const dest = path35.join(root, MOTION_DEST, rel);
|
|
25016
|
+
const exists5 = await fs35.access(dest).then(() => true, () => false);
|
|
23779
25017
|
if (!opts.force && exists5) {
|
|
23780
25018
|
skipped++;
|
|
23781
|
-
log.dim(` skipped ${
|
|
25019
|
+
log.dim(` skipped ${path35.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
23782
25020
|
continue;
|
|
23783
25021
|
}
|
|
23784
|
-
await
|
|
23785
|
-
await
|
|
25022
|
+
await fs35.mkdir(path35.dirname(dest), { recursive: true });
|
|
25023
|
+
await fs35.copyFile(path35.join(srcDir, rel), dest);
|
|
23786
25024
|
copied++;
|
|
23787
|
-
log.dim(` ${
|
|
25025
|
+
log.dim(` ${path35.join(MOTION_DEST, rel)}`);
|
|
23788
25026
|
}
|
|
23789
25027
|
} catch (err) {
|
|
23790
25028
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -23825,7 +25063,7 @@ async function motionConstraints(opts, log) {
|
|
|
23825
25063
|
}
|
|
23826
25064
|
const doc = directionConstraint(dir, speed, duration);
|
|
23827
25065
|
const out = opts.out ?? "constraints.json";
|
|
23828
|
-
await
|
|
25066
|
+
await fs35.writeFile(out, JSON.stringify(doc));
|
|
23829
25067
|
if (opts.json) {
|
|
23830
25068
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
23831
25069
|
return;
|
|
@@ -23862,14 +25100,14 @@ async function runMotion(opts) {
|
|
|
23862
25100
|
}
|
|
23863
25101
|
|
|
23864
25102
|
// src/commands/blender.ts
|
|
23865
|
-
import
|
|
23866
|
-
import
|
|
23867
|
-
var
|
|
25103
|
+
import fs36 from "fs/promises";
|
|
25104
|
+
import path36 from "path";
|
|
25105
|
+
var SUBS3 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
|
|
23868
25106
|
var DEFAULT_OUT_DIR = "assets/blender";
|
|
23869
25107
|
async function writeB64(dir, name, b64) {
|
|
23870
|
-
await
|
|
23871
|
-
const p =
|
|
23872
|
-
await
|
|
25108
|
+
await fs36.mkdir(dir, { recursive: true });
|
|
25109
|
+
const p = path36.join(dir, name);
|
|
25110
|
+
await fs36.writeFile(p, Buffer.from(b64, "base64"));
|
|
23873
25111
|
return p;
|
|
23874
25112
|
}
|
|
23875
25113
|
function reportScene(log, s) {
|
|
@@ -23881,8 +25119,8 @@ function reportScene(log, s) {
|
|
|
23881
25119
|
async function runBlender(opts) {
|
|
23882
25120
|
const log = createLogger();
|
|
23883
25121
|
const sub = opts.sub;
|
|
23884
|
-
if (!sub || !
|
|
23885
|
-
log.error(`Usage: genex blender <${
|
|
25122
|
+
if (!sub || !SUBS3.includes(sub)) {
|
|
25123
|
+
log.error(`Usage: genex blender <${SUBS3.join("|")}>`);
|
|
23886
25124
|
return 1;
|
|
23887
25125
|
}
|
|
23888
25126
|
if (sub === "serve") {
|
|
@@ -23953,7 +25191,7 @@ async function runBlender(opts) {
|
|
|
23953
25191
|
log.plain(rest.join("\n"));
|
|
23954
25192
|
return 1;
|
|
23955
25193
|
}
|
|
23956
|
-
const outDir =
|
|
25194
|
+
const outDir = path36.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
|
|
23957
25195
|
const mode = opts.mode;
|
|
23958
25196
|
if (mode !== void 0 && !isRenderMode(mode)) {
|
|
23959
25197
|
log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
|
|
@@ -23993,14 +25231,14 @@ async function runBlender(opts) {
|
|
|
23993
25231
|
return 0;
|
|
23994
25232
|
}
|
|
23995
25233
|
case "export": {
|
|
23996
|
-
const target = opts.out ??
|
|
25234
|
+
const target = opts.out ?? path36.join(outDir, "scene.glb");
|
|
23997
25235
|
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
23998
25236
|
if (!r.glbBase64) {
|
|
23999
25237
|
log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
|
|
24000
25238
|
return 1;
|
|
24001
25239
|
}
|
|
24002
|
-
await
|
|
24003
|
-
await
|
|
25240
|
+
await fs36.mkdir(path36.dirname(target), { recursive: true });
|
|
25241
|
+
await fs36.writeFile(target, Buffer.from(r.glbBase64, "base64"));
|
|
24004
25242
|
log.success(`Exported ${r.bytes ?? 0} bytes`);
|
|
24005
25243
|
log.plain(` ${c.cyan(target)}`);
|
|
24006
25244
|
return 0;
|
|
@@ -24033,12 +25271,12 @@ async function runBlender(opts) {
|
|
|
24033
25271
|
return 1;
|
|
24034
25272
|
}
|
|
24035
25273
|
try {
|
|
24036
|
-
script = await
|
|
25274
|
+
script = await fs36.readFile(opts.input, "utf8");
|
|
24037
25275
|
} catch {
|
|
24038
25276
|
log.error(`Can't read ${opts.input}`);
|
|
24039
25277
|
return 1;
|
|
24040
25278
|
}
|
|
24041
|
-
label =
|
|
25279
|
+
label = path36.basename(opts.input);
|
|
24042
25280
|
}
|
|
24043
25281
|
const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
|
|
24044
25282
|
if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
|
|
@@ -24157,9 +25395,9 @@ print(f"castle: {n} objects")
|
|
|
24157
25395
|
`;
|
|
24158
25396
|
|
|
24159
25397
|
// src/commands/asset-new.ts
|
|
24160
|
-
import
|
|
25398
|
+
import fs37 from "fs";
|
|
24161
25399
|
import fsp from "fs/promises";
|
|
24162
|
-
import
|
|
25400
|
+
import path37 from "path";
|
|
24163
25401
|
import { pathToFileURL } from "url";
|
|
24164
25402
|
var EXTRA_FILES = [
|
|
24165
25403
|
"genex-asset.example.json",
|
|
@@ -24253,7 +25491,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
24253
25491
|
}
|
|
24254
25492
|
async function runAssetNew(options) {
|
|
24255
25493
|
const log = createLogger();
|
|
24256
|
-
const cwd = options.dir ?
|
|
25494
|
+
const cwd = options.dir ? path37.resolve(options.dir) : process.cwd();
|
|
24257
25495
|
const slug = options.assetSlug;
|
|
24258
25496
|
if (!slug) {
|
|
24259
25497
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -24263,14 +25501,14 @@ async function runAssetNew(options) {
|
|
|
24263
25501
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
24264
25502
|
return 1;
|
|
24265
25503
|
}
|
|
24266
|
-
const templateDir =
|
|
24267
|
-
if (!
|
|
25504
|
+
const templateDir = path37.join(getTemplatesDir(), "asset-viewer");
|
|
25505
|
+
if (!fs37.existsSync(templateDir)) {
|
|
24268
25506
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
24269
25507
|
return 1;
|
|
24270
25508
|
}
|
|
24271
|
-
const manifestTools = await import(pathToFileURL(
|
|
25509
|
+
const manifestTools = await import(pathToFileURL(path37.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
24272
25510
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
24273
|
-
const lockPath =
|
|
25511
|
+
const lockPath = path37.join(templateDir, "shared-files.sha256.json");
|
|
24274
25512
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
24275
25513
|
const actual = hashSharedFiles(templateDir);
|
|
24276
25514
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -24283,8 +25521,8 @@ async function runAssetNew(options) {
|
|
|
24283
25521
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
24284
25522
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
24285
25523
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
24286
|
-
const outDir =
|
|
24287
|
-
if (
|
|
25524
|
+
const outDir = path37.resolve(cwd, options.out ?? slug);
|
|
25525
|
+
if (fs37.existsSync(outDir) && fs37.readdirSync(outDir).length > 0 && !options.force) {
|
|
24288
25526
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
24289
25527
|
return 1;
|
|
24290
25528
|
}
|
|
@@ -24312,24 +25550,24 @@ async function runAssetNew(options) {
|
|
|
24312
25550
|
};
|
|
24313
25551
|
await fsp.mkdir(outDir, { recursive: true });
|
|
24314
25552
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
24315
|
-
const to =
|
|
24316
|
-
await fsp.mkdir(
|
|
24317
|
-
await fsp.copyFile(
|
|
25553
|
+
const to = path37.join(outDir, rel);
|
|
25554
|
+
await fsp.mkdir(path37.dirname(to), { recursive: true });
|
|
25555
|
+
await fsp.copyFile(path37.join(templateDir, rel), to);
|
|
24318
25556
|
}
|
|
24319
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
25557
|
+
const pkg = fillTemplate(await fsp.readFile(path37.join(templateDir, "package.json"), "utf8"), {
|
|
24320
25558
|
slug,
|
|
24321
25559
|
name,
|
|
24322
25560
|
version
|
|
24323
25561
|
});
|
|
24324
|
-
await fsp.writeFile(
|
|
24325
|
-
await fsp.writeFile(
|
|
24326
|
-
await fsp.writeFile(
|
|
25562
|
+
await fsp.writeFile(path37.join(outDir, "package.json"), pkg, "utf8");
|
|
25563
|
+
await fsp.writeFile(path37.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
25564
|
+
await fsp.writeFile(path37.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
24327
25565
|
await fsp.writeFile(
|
|
24328
|
-
|
|
25566
|
+
path37.join(outDir, "DESIGN.md"),
|
|
24329
25567
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
24330
25568
|
"utf8"
|
|
24331
25569
|
);
|
|
24332
|
-
const placeholder = await fsp.readFile(
|
|
25570
|
+
const placeholder = await fsp.readFile(path37.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
24333
25571
|
const seeded = seedAssetSource(placeholder, {
|
|
24334
25572
|
slug,
|
|
24335
25573
|
name,
|
|
@@ -24340,8 +25578,8 @@ async function runAssetNew(options) {
|
|
|
24340
25578
|
pascalCase
|
|
24341
25579
|
});
|
|
24342
25580
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
24343
|
-
await fsp.mkdir(
|
|
24344
|
-
await fsp.writeFile(
|
|
25581
|
+
await fsp.mkdir(path37.join(outDir, "src", "asset"), { recursive: true });
|
|
25582
|
+
await fsp.writeFile(path37.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
24345
25583
|
const copied = hashSharedFiles(outDir);
|
|
24346
25584
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
24347
25585
|
if (mismatched.length) {
|
|
@@ -24349,7 +25587,7 @@ async function runAssetNew(options) {
|
|
|
24349
25587
|
return 1;
|
|
24350
25588
|
}
|
|
24351
25589
|
await fsp.writeFile(
|
|
24352
|
-
|
|
25590
|
+
path37.join(outDir, PARITY_FILENAME),
|
|
24353
25591
|
JSON.stringify(
|
|
24354
25592
|
{
|
|
24355
25593
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -24393,16 +25631,16 @@ async function runAssetNew(options) {
|
|
|
24393
25631
|
}
|
|
24394
25632
|
|
|
24395
25633
|
// src/commands/tools.ts
|
|
24396
|
-
import
|
|
25634
|
+
import path40 from "path";
|
|
24397
25635
|
|
|
24398
25636
|
// src/commands/doctor.ts
|
|
24399
|
-
import
|
|
25637
|
+
import path39 from "path";
|
|
24400
25638
|
|
|
24401
25639
|
// src/commands/llm.ts
|
|
24402
|
-
import
|
|
24403
|
-
import
|
|
25640
|
+
import fs38 from "fs/promises";
|
|
25641
|
+
import path38 from "path";
|
|
24404
25642
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
24405
|
-
var
|
|
25643
|
+
var SUBS4 = ["models", "bench", "price", "status", "cancel"];
|
|
24406
25644
|
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN credits \u2014 your balance, not a player's, and a spend this build's asset allowance does not count. Re-run with --max-credits <n> --user-approved only after the person at the keyboard agreed to the number.";
|
|
24407
25645
|
var LLM_MAX_COINS_DEPRECATED = "--max-coins is deprecated: it is read as --max-credits, the same number \u2014 a bench spends credits now.";
|
|
24408
25646
|
function benchBalanceShortLine(spendable, maxCredits) {
|
|
@@ -24548,8 +25786,8 @@ async function runLlm(opts = {}) {
|
|
|
24548
25786
|
else log.error(message);
|
|
24549
25787
|
process.exitCode = 1;
|
|
24550
25788
|
};
|
|
24551
|
-
if (!
|
|
24552
|
-
fail5(`llm ${sub}`, `Unknown subcommand ${sub}. Use: ${
|
|
25789
|
+
if (!SUBS4.includes(sub)) {
|
|
25790
|
+
fail5(`llm ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS4.join(", ")}.`);
|
|
24553
25791
|
return;
|
|
24554
25792
|
}
|
|
24555
25793
|
if (sub === "price") {
|
|
@@ -24921,7 +26159,7 @@ async function runBench(args) {
|
|
|
24921
26159
|
let schema;
|
|
24922
26160
|
if (opts.schemaPath) {
|
|
24923
26161
|
try {
|
|
24924
|
-
schema = JSON.parse(await
|
|
26162
|
+
schema = JSON.parse(await fs38.readFile(path38.resolve(cwd, opts.schemaPath), "utf8"));
|
|
24925
26163
|
} catch (err) {
|
|
24926
26164
|
benchFailed(opts, log, `Couldn't read --schema ${opts.schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24927
26165
|
return;
|
|
@@ -25303,9 +26541,9 @@ async function pollSettled(call2, url, timeoutSec) {
|
|
|
25303
26541
|
}
|
|
25304
26542
|
async function saveBench(cwd, record) {
|
|
25305
26543
|
try {
|
|
25306
|
-
const file =
|
|
25307
|
-
await
|
|
25308
|
-
await
|
|
26544
|
+
const file = path38.join(cwd, LLM_BENCH_FILE);
|
|
26545
|
+
await fs38.mkdir(path38.dirname(file), { recursive: true });
|
|
26546
|
+
await fs38.writeFile(file, JSON.stringify(record, null, 2) + "\n");
|
|
25309
26547
|
return LLM_BENCH_FILE;
|
|
25310
26548
|
} catch {
|
|
25311
26549
|
return null;
|
|
@@ -25412,8 +26650,8 @@ async function reportStatus(args) {
|
|
|
25412
26650
|
for (const row of rows) {
|
|
25413
26651
|
const state = rowState(row);
|
|
25414
26652
|
const mark = state === "active" ? c.cyan("\u25CF") : state === "awaiting bill" ? c.yellow("\u25D0") : c.green("\u25CB");
|
|
25415
|
-
const
|
|
25416
|
-
const reserved =
|
|
26653
|
+
const money = rowMoney(row);
|
|
26654
|
+
const reserved = money.reserved === null ? "reserved \u2014" : `${money.reserved} ${money.unit} reserved`;
|
|
25417
26655
|
log.plain(
|
|
25418
26656
|
` ${mark} ${row.id ?? "?"} ${row.modelId ?? "?"} ${state.padEnd(13)} ${formatAge(row.createdAt, now).padStart(4)} old ${reserved}${row.error ? ` ${row.error}` : ""}${rowHoldsSlot(row) ? "" : c.dim(" (no slot)")}`
|
|
25419
26657
|
);
|
|
@@ -25488,8 +26726,8 @@ async function cancelCall(args) {
|
|
|
25488
26726
|
log.dim(` Nothing to cancel: what it reserved releases when the bill resolves, and it stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`);
|
|
25489
26727
|
if (row.providerMessage?.trim()) log.dim(` Provider said: ${row.providerMessage.trim()}`);
|
|
25490
26728
|
} else {
|
|
25491
|
-
const
|
|
25492
|
-
log.dim(` ${
|
|
26729
|
+
const money = rowMoney(row);
|
|
26730
|
+
log.dim(` ${money.charged === null ? "charge unknown" : `${money.charged} ${money.unit} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
|
|
25493
26731
|
}
|
|
25494
26732
|
}
|
|
25495
26733
|
return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
|
|
@@ -25518,7 +26756,7 @@ async function cancelCall(args) {
|
|
|
25518
26756
|
async function readSavedBench(cwd) {
|
|
25519
26757
|
let parsed;
|
|
25520
26758
|
try {
|
|
25521
|
-
parsed = JSON.parse(await
|
|
26759
|
+
parsed = JSON.parse(await fs38.readFile(path38.join(cwd, LLM_BENCH_FILE), "utf8"));
|
|
25522
26760
|
} catch {
|
|
25523
26761
|
return null;
|
|
25524
26762
|
}
|
|
@@ -25950,7 +27188,7 @@ async function fetchLegalStatus(apiUrl, token) {
|
|
|
25950
27188
|
}
|
|
25951
27189
|
async function firstSkillsMarker() {
|
|
25952
27190
|
for (const target of resolveAgentTargets()) {
|
|
25953
|
-
const marker = await readSkillsMarker(
|
|
27191
|
+
const marker = await readSkillsMarker(path39.join(target.baseDir, "skills"));
|
|
25954
27192
|
if (marker) return marker;
|
|
25955
27193
|
}
|
|
25956
27194
|
return null;
|
|
@@ -26002,11 +27240,11 @@ async function runTools(opts) {
|
|
|
26002
27240
|
let totalUpdated = 0;
|
|
26003
27241
|
const refused = [];
|
|
26004
27242
|
for (const t of targets) {
|
|
26005
|
-
const dest =
|
|
27243
|
+
const dest = path40.join(t.baseDir, "skills");
|
|
26006
27244
|
let copied;
|
|
26007
27245
|
let updated;
|
|
26008
27246
|
try {
|
|
26009
|
-
({ copied, updated } = await copyTemplates(
|
|
27247
|
+
({ copied, updated } = await copyTemplates(path40.join(templatesDir, "skills"), dest, {
|
|
26010
27248
|
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
26011
27249
|
}));
|
|
26012
27250
|
await pruneRemovedSkills(dest, log);
|
|
@@ -26078,8 +27316,8 @@ async function runTools(opts) {
|
|
|
26078
27316
|
}
|
|
26079
27317
|
|
|
26080
27318
|
// src/commands/budget.ts
|
|
26081
|
-
import
|
|
26082
|
-
import
|
|
27319
|
+
import fs39 from "fs/promises";
|
|
27320
|
+
import path41 from "path";
|
|
26083
27321
|
var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
|
|
26084
27322
|
var BUDGET_APPROVAL_REQUIRED = "STOP: raising the asset allowance requires the player's explicit approval. Re-run with --user-approved only after they agreed to the number.";
|
|
26085
27323
|
function internalKind(kind) {
|
|
@@ -26087,7 +27325,7 @@ function internalKind(kind) {
|
|
|
26087
27325
|
}
|
|
26088
27326
|
async function isGenexProject(cwd) {
|
|
26089
27327
|
try {
|
|
26090
|
-
await
|
|
27328
|
+
await fs39.access(path41.join(cwd, ".genex"));
|
|
26091
27329
|
return true;
|
|
26092
27330
|
} catch {
|
|
26093
27331
|
return false;
|
|
@@ -26236,39 +27474,39 @@ async function runBudget(opts = {}) {
|
|
|
26236
27474
|
}
|
|
26237
27475
|
|
|
26238
27476
|
// src/commands/player.ts
|
|
26239
|
-
import
|
|
27477
|
+
import fs44 from "fs";
|
|
26240
27478
|
import fsp2 from "fs/promises";
|
|
26241
27479
|
import os12 from "os";
|
|
26242
|
-
import
|
|
27480
|
+
import path47 from "path";
|
|
26243
27481
|
import { spawn as spawn8 } from "child_process";
|
|
26244
27482
|
|
|
26245
27483
|
// src/lib/player/paths.ts
|
|
26246
27484
|
import os8 from "os";
|
|
26247
|
-
import
|
|
27485
|
+
import path42 from "path";
|
|
26248
27486
|
var PLAYER_DRY_RUN_ENV = "GENEX_PLAYER_DRY_RUN";
|
|
26249
27487
|
function playerDryRun() {
|
|
26250
27488
|
const raw = process.env[PLAYER_DRY_RUN_ENV];
|
|
26251
27489
|
return raw === "1" || raw === "true";
|
|
26252
27490
|
}
|
|
26253
27491
|
function playerPaths(home = os8.homedir()) {
|
|
26254
|
-
const dir = home === os8.homedir() ? getGenexDir() :
|
|
26255
|
-
const logDir =
|
|
27492
|
+
const dir = home === os8.homedir() ? getGenexDir() : path42.join(home, ".genex");
|
|
27493
|
+
const logDir = path42.join(dir, "logs");
|
|
26256
27494
|
return {
|
|
26257
|
-
credential:
|
|
26258
|
-
state:
|
|
26259
|
-
relaunch:
|
|
27495
|
+
credential: path42.join(dir, "player.json"),
|
|
27496
|
+
state: path42.join(dir, "player-state.json"),
|
|
27497
|
+
relaunch: path42.join(dir, "player-relaunch.json"),
|
|
26260
27498
|
logDir,
|
|
26261
|
-
log:
|
|
26262
|
-
errorLog:
|
|
27499
|
+
log: path42.join(logDir, "player.log"),
|
|
27500
|
+
errorLog: path42.join(logDir, "player.error.log")
|
|
26263
27501
|
};
|
|
26264
27502
|
}
|
|
26265
27503
|
|
|
26266
27504
|
// src/lib/player/store.ts
|
|
26267
|
-
import
|
|
26268
|
-
import
|
|
27505
|
+
import fs40 from "fs/promises";
|
|
27506
|
+
import path43 from "path";
|
|
26269
27507
|
async function readPlayerInstall(home) {
|
|
26270
27508
|
try {
|
|
26271
|
-
const raw = await
|
|
27509
|
+
const raw = await fs40.readFile(playerPaths(home).credential, "utf8");
|
|
26272
27510
|
const parsed = JSON.parse(raw);
|
|
26273
27511
|
if (!parsed || typeof parsed.token !== "string" || typeof parsed.apiUrl !== "string") return null;
|
|
26274
27512
|
return parsed;
|
|
@@ -26298,7 +27536,7 @@ function describeInstall(install2) {
|
|
|
26298
27536
|
}
|
|
26299
27537
|
async function readPlayerState(home) {
|
|
26300
27538
|
try {
|
|
26301
|
-
return JSON.parse(await
|
|
27539
|
+
return JSON.parse(await fs40.readFile(playerPaths(home).state, "utf8"));
|
|
26302
27540
|
} catch {
|
|
26303
27541
|
return null;
|
|
26304
27542
|
}
|
|
@@ -26306,8 +27544,8 @@ async function readPlayerState(home) {
|
|
|
26306
27544
|
async function writePlayerState(state, home) {
|
|
26307
27545
|
const file = playerPaths(home).state;
|
|
26308
27546
|
try {
|
|
26309
|
-
await
|
|
26310
|
-
await
|
|
27547
|
+
await fs40.mkdir(path43.dirname(file), { recursive: true, mode: 448 });
|
|
27548
|
+
await fs40.writeFile(file, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
26311
27549
|
} catch {
|
|
26312
27550
|
}
|
|
26313
27551
|
}
|
|
@@ -26315,8 +27553,8 @@ async function removePlayerFiles(home) {
|
|
|
26315
27553
|
const paths = playerPaths(home);
|
|
26316
27554
|
const removed = [];
|
|
26317
27555
|
for (const file of [paths.credential, paths.state, paths.relaunch]) {
|
|
26318
|
-
const existed = await
|
|
26319
|
-
await
|
|
27556
|
+
const existed = await fs40.stat(file).then(() => true, () => false);
|
|
27557
|
+
await fs40.rm(file, { force: true }).catch(() => {
|
|
26320
27558
|
});
|
|
26321
27559
|
if (existed) removed.push(file);
|
|
26322
27560
|
}
|
|
@@ -26450,14 +27688,14 @@ var PlayerClient = class {
|
|
|
26450
27688
|
this.version = options.version ?? getCliVersion();
|
|
26451
27689
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
26452
27690
|
}
|
|
26453
|
-
async call(
|
|
27691
|
+
async call(path49, init = {}, provider) {
|
|
26454
27692
|
const headers = {
|
|
26455
27693
|
Authorization: `Bearer ${this.token}`,
|
|
26456
27694
|
[PLAYER_VERSION_HEADER]: this.version,
|
|
26457
27695
|
...provider ? { [EXTERNAL_PROVIDER_HEADER]: provider } : {},
|
|
26458
27696
|
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
26459
27697
|
};
|
|
26460
|
-
const res = await apiFetch(`${this.apiUrl}${
|
|
27698
|
+
const res = await apiFetch(`${this.apiUrl}${path49}`, {
|
|
26461
27699
|
...init,
|
|
26462
27700
|
headers,
|
|
26463
27701
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -26526,10 +27764,10 @@ var PlayerClient = class {
|
|
|
26526
27764
|
|
|
26527
27765
|
// src/lib/player/runners.ts
|
|
26528
27766
|
import { spawn as spawn6, spawnSync } from "child_process";
|
|
26529
|
-
import
|
|
27767
|
+
import fs41 from "fs/promises";
|
|
26530
27768
|
import fsSync from "fs";
|
|
26531
27769
|
import os9 from "os";
|
|
26532
|
-
import
|
|
27770
|
+
import path44 from "path";
|
|
26533
27771
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
26534
27772
|
var WATCHER_RUNNERS = ["claude", "codex"];
|
|
26535
27773
|
var RUNNER_FOR_FAMILY = { claude: "claude", chatgpt: "codex" };
|
|
@@ -26587,7 +27825,7 @@ function resolveOnPath(name, env = process.env, platform = process.platform) {
|
|
|
26587
27825
|
const candidates = win ? [`${name}.cmd`, `${name}.exe`, `${name}.bat`, name] : [name];
|
|
26588
27826
|
for (const dir of dirs) {
|
|
26589
27827
|
for (const candidate of candidates) {
|
|
26590
|
-
const file =
|
|
27828
|
+
const file = path44.join(dir, candidate);
|
|
26591
27829
|
try {
|
|
26592
27830
|
const stat = fsSync.statSync(file);
|
|
26593
27831
|
if (!stat.isFile()) continue;
|
|
@@ -26751,17 +27989,17 @@ async function runJob(job, options = {}) {
|
|
|
26751
27989
|
if (!resolved) {
|
|
26752
27990
|
return { ok: false, release: "runner_unavailable", detail: `missing:${runner}`, durationMs: Date.now() - started };
|
|
26753
27991
|
}
|
|
26754
|
-
const cwd =
|
|
26755
|
-
await
|
|
26756
|
-
const outFile =
|
|
27992
|
+
const cwd = path44.join(os9.tmpdir(), `genex-player-${randomUUID3()}`);
|
|
27993
|
+
await fs41.mkdir(cwd, { recursive: true, mode: 448 });
|
|
27994
|
+
const outFile = path44.join(cwd, "result.out");
|
|
26757
27995
|
let argv;
|
|
26758
27996
|
if (runner === "claude") {
|
|
26759
27997
|
argv = claudeArgv(job, policy.claude ?? FALLBACK_POLICY.claude ?? {});
|
|
26760
27998
|
} else {
|
|
26761
27999
|
let schemaFile;
|
|
26762
28000
|
if (job.outputFormat === "json" && job.schema !== void 0) {
|
|
26763
|
-
schemaFile =
|
|
26764
|
-
await
|
|
28001
|
+
schemaFile = path44.join(cwd, "schema.json");
|
|
28002
|
+
await fs41.writeFile(schemaFile, JSON.stringify(job.schema), { mode: 384 });
|
|
26765
28003
|
}
|
|
26766
28004
|
argv = codexArgv(job, policy.codex ?? FALLBACK_POLICY.codex ?? {}, { cwd, outFile, ...schemaFile ? { schemaFile } : {} });
|
|
26767
28005
|
}
|
|
@@ -26825,7 +28063,7 @@ async function runJob(job, options = {}) {
|
|
|
26825
28063
|
if (!read2.ok) return { ok: false, release: "failed", detail: read2.detail, exitCode: code, durationMs };
|
|
26826
28064
|
return { ok: true, output: read2.output, exitCode: code, durationMs };
|
|
26827
28065
|
}
|
|
26828
|
-
const text = await
|
|
28066
|
+
const text = await fs41.readFile(outFile, "utf8").catch(() => "");
|
|
26829
28067
|
if (code !== 0 && !text.trim()) {
|
|
26830
28068
|
const classified = classifyFailure(`${stderrText} exit ${code}`);
|
|
26831
28069
|
return { ok: false, ...classified, detail: `exit_${code}`, exitCode: code, durationMs };
|
|
@@ -26834,15 +28072,15 @@ async function runJob(job, options = {}) {
|
|
|
26834
28072
|
if (!read.ok) return { ok: false, release: "failed", detail: read.detail, exitCode: code, durationMs };
|
|
26835
28073
|
return { ok: true, output: read.output, exitCode: code, durationMs };
|
|
26836
28074
|
} finally {
|
|
26837
|
-
await
|
|
28075
|
+
await fs41.rm(cwd, { recursive: true, force: true }).catch(() => {
|
|
26838
28076
|
});
|
|
26839
28077
|
}
|
|
26840
28078
|
}
|
|
26841
28079
|
|
|
26842
28080
|
// src/lib/player/service.ts
|
|
26843
|
-
import
|
|
28081
|
+
import fs42 from "fs/promises";
|
|
26844
28082
|
import os10 from "os";
|
|
26845
|
-
import
|
|
28083
|
+
import path45 from "path";
|
|
26846
28084
|
import { execFile } from "child_process";
|
|
26847
28085
|
import { promisify } from "util";
|
|
26848
28086
|
var run2 = promisify(execFile);
|
|
@@ -26859,9 +28097,9 @@ function servicePlatform(platform = process.platform) {
|
|
|
26859
28097
|
}
|
|
26860
28098
|
function servicePaths(platform, home = os10.homedir()) {
|
|
26861
28099
|
const { log, errorLog } = playerPaths(home);
|
|
26862
|
-
if (platform === "darwin") return { log, errorLog, unit:
|
|
26863
|
-
if (platform === "linux") return { log, errorLog, unit:
|
|
26864
|
-
return { log, errorLog, unit:
|
|
28100
|
+
if (platform === "darwin") return { log, errorLog, unit: path45.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
|
|
28101
|
+
if (platform === "linux") return { log, errorLog, unit: path45.join(home, ".config", "systemd", "user", SYSTEMD_UNIT) };
|
|
28102
|
+
return { log, errorLog, unit: path45.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "genex-player.cmd") };
|
|
26865
28103
|
}
|
|
26866
28104
|
function xmlEscape(value) {
|
|
26867
28105
|
return value.replace(/[<>&'"]/g, (ch) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[ch]);
|
|
@@ -26940,9 +28178,9 @@ function autostartCommand(opts) {
|
|
|
26940
28178
|
}
|
|
26941
28179
|
async function installService(input, platform = servicePlatform()) {
|
|
26942
28180
|
const paths = servicePaths(platform, input.home);
|
|
26943
|
-
await
|
|
26944
|
-
await
|
|
26945
|
-
await
|
|
28181
|
+
await fs42.mkdir(path45.dirname(paths.unit), { recursive: true });
|
|
28182
|
+
await fs42.mkdir(path45.dirname(paths.log), { recursive: true, mode: 448 });
|
|
28183
|
+
await fs42.writeFile(paths.unit, unitFile(platform, input, paths), { mode: platform === "win32" ? 420 : 384 });
|
|
26946
28184
|
if (playerDryRun()) return { unit: paths.unit, loaded: false, detail: "dry run \u2014 the unit was written, the system was not asked" };
|
|
26947
28185
|
if (platform === "darwin") {
|
|
26948
28186
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
@@ -26971,7 +28209,7 @@ async function installService(input, platform = servicePlatform()) {
|
|
|
26971
28209
|
}
|
|
26972
28210
|
async function serviceInstalled(home = os10.homedir(), platform = servicePlatform()) {
|
|
26973
28211
|
const paths = servicePaths(platform, home);
|
|
26974
|
-
return await
|
|
28212
|
+
return await fs42.stat(paths.unit).then(() => paths.unit, () => null);
|
|
26975
28213
|
}
|
|
26976
28214
|
async function stopService(home = os10.homedir(), platform = servicePlatform()) {
|
|
26977
28215
|
const paths = servicePaths(platform, home);
|
|
@@ -27001,15 +28239,15 @@ async function uninstallService(home = os10.homedir(), platform = servicePlatfor
|
|
|
27001
28239
|
});
|
|
27002
28240
|
}
|
|
27003
28241
|
}
|
|
27004
|
-
await
|
|
28242
|
+
await fs42.rm(paths.unit, { force: true }).catch(() => {
|
|
27005
28243
|
});
|
|
27006
28244
|
return { unit: paths.unit, loaded: false, detail: present ? "removed" : "nothing installed" };
|
|
27007
28245
|
}
|
|
27008
28246
|
|
|
27009
28247
|
// src/lib/player/relaunch.ts
|
|
27010
|
-
import
|
|
28248
|
+
import fs43 from "fs/promises";
|
|
27011
28249
|
import fsSync2 from "fs";
|
|
27012
|
-
import
|
|
28250
|
+
import path46 from "path";
|
|
27013
28251
|
import { spawn as spawn7 } from "child_process";
|
|
27014
28252
|
var RELAUNCH_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
27015
28253
|
function relaunchPlan(input) {
|
|
@@ -27037,7 +28275,7 @@ function relaunchPossible() {
|
|
|
27037
28275
|
}
|
|
27038
28276
|
async function relaunchOnCooldown(home, now = Date.now()) {
|
|
27039
28277
|
try {
|
|
27040
|
-
const raw = await
|
|
28278
|
+
const raw = await fs43.readFile(playerPaths(home).relaunch, "utf8");
|
|
27041
28279
|
const at = Date.parse(JSON.parse(raw).at ?? "");
|
|
27042
28280
|
return Number.isFinite(at) && now - at < RELAUNCH_COOLDOWN_MS;
|
|
27043
28281
|
} catch {
|
|
@@ -27047,8 +28285,8 @@ async function relaunchOnCooldown(home, now = Date.now()) {
|
|
|
27047
28285
|
async function markRelaunch(version, home) {
|
|
27048
28286
|
const file = playerPaths(home).relaunch;
|
|
27049
28287
|
try {
|
|
27050
|
-
await
|
|
27051
|
-
await
|
|
28288
|
+
await fs43.mkdir(path46.dirname(file), { recursive: true, mode: 448 });
|
|
28289
|
+
await fs43.writeFile(file, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), version }) + "\n", { mode: 384 });
|
|
27052
28290
|
} catch {
|
|
27053
28291
|
}
|
|
27054
28292
|
}
|
|
@@ -27072,7 +28310,7 @@ async function relaunchWatcher(input) {
|
|
|
27072
28310
|
let err;
|
|
27073
28311
|
const paths = playerPaths(input.home);
|
|
27074
28312
|
try {
|
|
27075
|
-
await
|
|
28313
|
+
await fs43.mkdir(paths.logDir, { recursive: true, mode: 448 });
|
|
27076
28314
|
out = fsSync2.openSync(paths.log, "a", 384);
|
|
27077
28315
|
err = fsSync2.openSync(paths.errorLog, "a", 384);
|
|
27078
28316
|
} catch {
|
|
@@ -27410,15 +28648,15 @@ function defaultLabel2() {
|
|
|
27410
28648
|
}
|
|
27411
28649
|
|
|
27412
28650
|
// src/commands/player.ts
|
|
27413
|
-
var
|
|
28651
|
+
var SUBS5 = ["install", "run", "status", "stop", "uninstall"];
|
|
27414
28652
|
var PLAYER_NOT_INSTALLED = "This machine is not set up to answer in-game model requests \u2014 run `genex player install` first.";
|
|
27415
28653
|
var PLAYER_REAUTH_REQUIRED = "Genex no longer accepts this machine's approval \u2014 run `genex player install` again.";
|
|
27416
28654
|
var PLAYER_NO_RUNNERS = "Neither `claude` nor `codex` is on this machine's PATH, so nothing can be answered yet. Install the one whose subscription you pay for, sign into it, and this watcher picks it up on its own.";
|
|
27417
28655
|
async function runPlayer(opts = {}) {
|
|
27418
28656
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
27419
28657
|
const sub = opts.name?.trim() || "status";
|
|
27420
|
-
if (!
|
|
27421
|
-
fail4(opts, log, `player ${sub}`, `Unknown subcommand ${sub}. Use: ${
|
|
28658
|
+
if (!SUBS5.includes(sub)) {
|
|
28659
|
+
fail4(opts, log, `player ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS5.join(", ")}.`);
|
|
27422
28660
|
return;
|
|
27423
28661
|
}
|
|
27424
28662
|
if (opts.dryRun) process.env[PLAYER_DRY_RUN_ENV] = "1";
|
|
@@ -27553,8 +28791,8 @@ async function spawnDetachedRun(apiUrl, opts) {
|
|
|
27553
28791
|
try {
|
|
27554
28792
|
const paths = playerPaths();
|
|
27555
28793
|
await fsp2.mkdir(paths.logDir, { recursive: true, mode: 448 });
|
|
27556
|
-
out =
|
|
27557
|
-
err =
|
|
28794
|
+
out = fs44.openSync(paths.log, "a", 384);
|
|
28795
|
+
err = fs44.openSync(paths.errorLog, "a", 384);
|
|
27558
28796
|
const args = [process.argv[1] ?? "", "player", "run", ...opts.apiUrl ? ["--api-url", apiUrl] : []];
|
|
27559
28797
|
const child = spawn8(process.execPath, args, { detached: true, stdio: ["ignore", out, err] });
|
|
27560
28798
|
child.on("error", () => {
|
|
@@ -27565,7 +28803,7 @@ async function spawnDetachedRun(apiUrl, opts) {
|
|
|
27565
28803
|
return false;
|
|
27566
28804
|
} finally {
|
|
27567
28805
|
for (const fd of [out, err]) if (fd !== void 0) try {
|
|
27568
|
-
|
|
28806
|
+
fs44.closeSync(fd);
|
|
27569
28807
|
} catch {
|
|
27570
28808
|
}
|
|
27571
28809
|
}
|
|
@@ -27764,7 +29002,7 @@ async function uninstall(opts, log) {
|
|
|
27764
29002
|
writeJsonLine2({ command: "player uninstall", status: "ok", error: null, unit: service.unit, detail: service.detail, removed });
|
|
27765
29003
|
return;
|
|
27766
29004
|
}
|
|
27767
|
-
log.success(`Removed: ${service.detail} (${
|
|
29005
|
+
log.success(`Removed: ${service.detail} (${path47.basename(service.unit)})`);
|
|
27768
29006
|
for (const file of removed) log.dim(` deleted ${file}`);
|
|
27769
29007
|
if (removed.length === 0) log.dim(" no credential was stored here");
|
|
27770
29008
|
log.plain("");
|
|
@@ -27782,46 +29020,46 @@ function writeJsonLine2(value) {
|
|
|
27782
29020
|
}
|
|
27783
29021
|
|
|
27784
29022
|
// src/lib/native-build.ts
|
|
27785
|
-
import { createHash as
|
|
29023
|
+
import { createHash as createHash4 } from "crypto";
|
|
27786
29024
|
import { createReadStream } from "fs";
|
|
27787
|
-
import
|
|
29025
|
+
import fs46 from "fs/promises";
|
|
27788
29026
|
import os13 from "os";
|
|
27789
|
-
import
|
|
29027
|
+
import path48 from "path";
|
|
27790
29028
|
import { execFile as execFile2 } from "child_process";
|
|
27791
29029
|
import { promisify as promisify2 } from "util";
|
|
27792
29030
|
|
|
27793
29031
|
// src/lib/native-zip.ts
|
|
27794
|
-
import
|
|
27795
|
-
import
|
|
27796
|
-
var
|
|
29032
|
+
import fs45 from "fs/promises";
|
|
29033
|
+
import zlib3 from "zlib";
|
|
29034
|
+
var EOCD_SIGNATURE2 = 101010256;
|
|
27797
29035
|
var EOCD64_LOCATOR_SIGNATURE = 117853008;
|
|
27798
29036
|
var EOCD64_SIGNATURE = 101075792;
|
|
27799
|
-
var
|
|
27800
|
-
var
|
|
29037
|
+
var CENTRAL_SIGNATURE2 = 33639248;
|
|
29038
|
+
var LOCAL_SIGNATURE2 = 67324752;
|
|
27801
29039
|
var EOCD_MIN = 22;
|
|
27802
|
-
var
|
|
29040
|
+
var COMMENT_MAX2 = 65535;
|
|
27803
29041
|
var ZIP64_EXTRA = 1;
|
|
27804
29042
|
var U32_MAX = 4294967295;
|
|
27805
29043
|
var U16_MAX = 65535;
|
|
27806
29044
|
var DIRECTORY_MAX_BYTES = 256 * 1024 * 1024;
|
|
27807
|
-
var
|
|
29045
|
+
var UNIX_HOST2 = 3;
|
|
27808
29046
|
var INPUT_MIN = 64 * 1024;
|
|
27809
29047
|
var INPUT_MAX = 1024 * 1024;
|
|
27810
|
-
var
|
|
29048
|
+
var S_IFMT2 = 61440;
|
|
27811
29049
|
var S_IFLNK = 40960;
|
|
27812
29050
|
var isDirectory = (entry) => entry.name.endsWith("/");
|
|
27813
|
-
var isSymlink = (entry) => entry.mode !== null && (entry.mode &
|
|
29051
|
+
var isSymlink = (entry) => entry.mode !== null && (entry.mode & S_IFMT2) === S_IFLNK;
|
|
27814
29052
|
async function readAt2(file, position, length) {
|
|
27815
29053
|
const buffer = Buffer.alloc(length);
|
|
27816
29054
|
const { bytesRead } = await file.read(buffer, 0, length, position);
|
|
27817
29055
|
return buffer.subarray(0, bytesRead);
|
|
27818
29056
|
}
|
|
27819
|
-
async function
|
|
27820
|
-
const tailLength = Math.min(size, EOCD_MIN +
|
|
29057
|
+
async function findDirectory2(file, size) {
|
|
29058
|
+
const tailLength = Math.min(size, EOCD_MIN + COMMENT_MAX2);
|
|
27821
29059
|
const tail = await readAt2(file, size - tailLength, tailLength);
|
|
27822
29060
|
let at = -1;
|
|
27823
29061
|
for (let i = tail.length - EOCD_MIN; i >= 0; i--) {
|
|
27824
|
-
if (tail.readUInt32LE(i) ===
|
|
29062
|
+
if (tail.readUInt32LE(i) === EOCD_SIGNATURE2) {
|
|
27825
29063
|
at = i;
|
|
27826
29064
|
break;
|
|
27827
29065
|
}
|
|
@@ -27863,15 +29101,15 @@ function applyZip64(extra, entry, overflowed) {
|
|
|
27863
29101
|
}
|
|
27864
29102
|
}
|
|
27865
29103
|
async function readZipEntries(filePath) {
|
|
27866
|
-
const file = await
|
|
29104
|
+
const file = await fs45.open(filePath, "r");
|
|
27867
29105
|
try {
|
|
27868
29106
|
const { size } = await file.stat();
|
|
27869
|
-
const directory = await
|
|
29107
|
+
const directory = await findDirectory2(file, size);
|
|
27870
29108
|
const data = await readAt2(file, directory.offset, directory.bytes);
|
|
27871
29109
|
const entries = [];
|
|
27872
29110
|
let at = 0;
|
|
27873
29111
|
while (entries.length < directory.count && at + 46 <= data.length) {
|
|
27874
|
-
if (data.readUInt32LE(at) !==
|
|
29112
|
+
if (data.readUInt32LE(at) !== CENTRAL_SIGNATURE2) throw new Error("broken zip directory entry");
|
|
27875
29113
|
const madeBy = data.readUInt16LE(at + 4);
|
|
27876
29114
|
const nameLength = data.readUInt16LE(at + 28);
|
|
27877
29115
|
const extraLength = data.readUInt16LE(at + 30);
|
|
@@ -27883,7 +29121,7 @@ async function readZipEntries(filePath) {
|
|
|
27883
29121
|
compressedSize: data.readUInt32LE(at + 20),
|
|
27884
29122
|
uncompressedSize: data.readUInt32LE(at + 24),
|
|
27885
29123
|
localOffset: data.readUInt32LE(at + 42),
|
|
27886
|
-
mode: madeBy >> 8 ===
|
|
29124
|
+
mode: madeBy >> 8 === UNIX_HOST2 ? external >>> 16 : null
|
|
27887
29125
|
};
|
|
27888
29126
|
applyZip64(data.subarray(at + 46 + nameLength, at + 46 + nameLength + extraLength), entry, {
|
|
27889
29127
|
uncompressed: entry.uncompressedSize === U32_MAX,
|
|
@@ -27900,10 +29138,10 @@ async function readZipEntries(filePath) {
|
|
|
27900
29138
|
}
|
|
27901
29139
|
}
|
|
27902
29140
|
async function readZipEntryHead(filePath, entry, maxBytes) {
|
|
27903
|
-
const file = await
|
|
29141
|
+
const file = await fs45.open(filePath, "r");
|
|
27904
29142
|
try {
|
|
27905
29143
|
const local = await readAt2(file, entry.localOffset, 30);
|
|
27906
|
-
if (local.length < 30 || local.readUInt32LE(0) !==
|
|
29144
|
+
if (local.length < 30 || local.readUInt32LE(0) !== LOCAL_SIGNATURE2) throw new Error(`broken zip entry ${entry.name}`);
|
|
27907
29145
|
const start = entry.localOffset + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
|
|
27908
29146
|
if (entry.method === 0) return await readAt2(file, start, Math.min(maxBytes, entry.uncompressedSize));
|
|
27909
29147
|
if (entry.method !== 8) throw new Error(`${entry.name} uses a compression this command cannot read`);
|
|
@@ -27915,7 +29153,7 @@ async function readZipEntryHead(filePath, entry, maxBytes) {
|
|
|
27915
29153
|
}
|
|
27916
29154
|
function inflateHead(input, maxBytes) {
|
|
27917
29155
|
return new Promise((resolve) => {
|
|
27918
|
-
const inflater =
|
|
29156
|
+
const inflater = zlib3.createInflateRaw();
|
|
27919
29157
|
const chunks = [];
|
|
27920
29158
|
let total = 0;
|
|
27921
29159
|
const finish2 = () => resolve(Buffer.concat(chunks).subarray(0, maxBytes));
|
|
@@ -27934,7 +29172,7 @@ function inflateHead(input, maxBytes) {
|
|
|
27934
29172
|
var run4 = promisify2(execFile2);
|
|
27935
29173
|
var NATIVE_PLATFORMS = ["mac", "windows", "linux"];
|
|
27936
29174
|
var NATIVE_ARCHES = ["arm64", "x64", "universal"];
|
|
27937
|
-
var
|
|
29175
|
+
var VERSION2 = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
27938
29176
|
var VERSION_MAX = 64;
|
|
27939
29177
|
var OS_VERSION = /^\d{1,4}(\.\d{1,6}){0,3}$/;
|
|
27940
29178
|
var ENGINE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
@@ -27964,7 +29202,7 @@ function rootApps(entries) {
|
|
|
27964
29202
|
}
|
|
27965
29203
|
function launchableExes(entries) {
|
|
27966
29204
|
const exes = entries.filter((e) => !isDirectory(e) && e.name.toLowerCase().endsWith(".exe"));
|
|
27967
|
-
const games = exes.filter((e) => e.name.split("/").length <= ENTRY_DEPTH_MAX && !HELPER_EXE.test(
|
|
29205
|
+
const games = exes.filter((e) => e.name.split("/").length <= ENTRY_DEPTH_MAX && !HELPER_EXE.test(path48.posix.basename(e.name)));
|
|
27968
29206
|
const depth = (name) => name.split("/").length;
|
|
27969
29207
|
return games.map((e) => e.name).sort((a, b) => depth(a) - depth(b) || a.localeCompare(b));
|
|
27970
29208
|
}
|
|
@@ -28046,7 +29284,7 @@ function exeArch(head) {
|
|
|
28046
29284
|
}
|
|
28047
29285
|
function engineOf(entries) {
|
|
28048
29286
|
const names2 = entries.map((e) => e.name.toLowerCase());
|
|
28049
|
-
const base = (name) =>
|
|
29287
|
+
const base = (name) => path48.posix.basename(name.replace(/\/$/, ""));
|
|
28050
29288
|
if (names2.some((n) => /^unityplayer\.(dll|dylib|so)$/.test(base(n)) || base(n) === "globalgamemanagers")) return "unity";
|
|
28051
29289
|
if (names2.some((n) => n.startsWith("engine/binaries/") || /\.app\/contents\/ue[45]?\//.test(n) || n.includes("/engine/binaries/")))
|
|
28052
29290
|
return "unreal";
|
|
@@ -28090,7 +29328,7 @@ async function describeNativeArchive(zipPath, asked = {}) {
|
|
|
28090
29328
|
try {
|
|
28091
29329
|
entries = await readZipEntries(zipPath);
|
|
28092
29330
|
} catch (err) {
|
|
28093
|
-
throw new NativeBuildError(`Couldn't read ${
|
|
29331
|
+
throw new NativeBuildError(`Couldn't read ${path48.basename(zipPath)} as a zip: ${err.message}`);
|
|
28094
29332
|
}
|
|
28095
29333
|
if (entries.length === 0) throw new NativeBuildError("The archive is empty.");
|
|
28096
29334
|
const platform = platformOf(entries, asked.platform);
|
|
@@ -28112,19 +29350,19 @@ async function describeNativeArchive(zipPath, asked = {}) {
|
|
|
28112
29350
|
...mac?.appVersion ? { appVersion: mac.appVersion } : {}
|
|
28113
29351
|
};
|
|
28114
29352
|
}
|
|
28115
|
-
function buildVersion(asked, appVersion,
|
|
29353
|
+
function buildVersion(asked, appVersion, sha2563) {
|
|
28116
29354
|
if (asked !== void 0) {
|
|
28117
|
-
if (asked.length > VERSION_MAX || !
|
|
29355
|
+
if (asked.length > VERSION_MAX || !VERSION2.test(asked)) {
|
|
28118
29356
|
throw new NativeBuildError(`--build-version "${asked}" may hold only letters, digits and . _ + - (at most ${VERSION_MAX}).`);
|
|
28119
29357
|
}
|
|
28120
29358
|
return asked;
|
|
28121
29359
|
}
|
|
28122
|
-
const hash =
|
|
29360
|
+
const hash = sha2563.slice(0, VERSION_SHA_CHARS);
|
|
28123
29361
|
const app = (appVersion ?? "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, VERSION_MAX - hash.length - 1);
|
|
28124
29362
|
return app ? `${app}+${hash}` : hash;
|
|
28125
29363
|
}
|
|
28126
29364
|
async function hashFile(filePath, onProgress) {
|
|
28127
|
-
const hash =
|
|
29365
|
+
const hash = createHash4("sha256");
|
|
28128
29366
|
let sizeBytes = 0;
|
|
28129
29367
|
for await (const chunk of createReadStream(filePath)) {
|
|
28130
29368
|
hash.update(chunk);
|
|
@@ -28135,13 +29373,13 @@ async function hashFile(filePath, onProgress) {
|
|
|
28135
29373
|
}
|
|
28136
29374
|
async function packTarget(dir) {
|
|
28137
29375
|
if (dir.toLowerCase().endsWith(".app")) return { target: dir, keepParent: true };
|
|
28138
|
-
const children = await
|
|
29376
|
+
const children = await fs46.readdir(dir, { withFileTypes: true });
|
|
28139
29377
|
const apps = children.filter((d) => d.isDirectory() && d.name.toLowerCase().endsWith(".app"));
|
|
28140
|
-
if (apps.length === 1) return { target:
|
|
29378
|
+
if (apps.length === 1) return { target: path48.join(dir, apps[0].name), keepParent: true };
|
|
28141
29379
|
return { target: dir, keepParent: false };
|
|
28142
29380
|
}
|
|
28143
29381
|
async function packNativeBuild(input) {
|
|
28144
|
-
const stat = await
|
|
29382
|
+
const stat = await fs46.stat(input).catch(() => null);
|
|
28145
29383
|
if (!stat) throw new NativeBuildError(`${input} does not exist.`);
|
|
28146
29384
|
if (stat.isFile()) {
|
|
28147
29385
|
if (!input.toLowerCase().endsWith(".zip")) throw new NativeBuildError("Pass a .zip, a .app, or the build's folder.");
|
|
@@ -28149,14 +29387,14 @@ async function packNativeBuild(input) {
|
|
|
28149
29387
|
} };
|
|
28150
29388
|
}
|
|
28151
29389
|
const { target, keepParent } = await packTarget(input);
|
|
28152
|
-
const dir = await
|
|
28153
|
-
const zipPath =
|
|
28154
|
-
const cleanup = () =>
|
|
29390
|
+
const dir = await fs46.mkdtemp(path48.join(os13.tmpdir(), "genex-native-"));
|
|
29391
|
+
const zipPath = path48.join(dir, `${path48.basename(target).replace(/[^A-Za-z0-9._-]+/g, "-") || "build"}.zip`);
|
|
29392
|
+
const cleanup = () => fs46.rm(dir, { recursive: true, force: true });
|
|
28155
29393
|
try {
|
|
28156
29394
|
if (process.platform === "darwin") {
|
|
28157
29395
|
await run4("ditto", ["-c", "-k", "--sequesterRsrc", ...keepParent ? ["--keepParent"] : [], target, zipPath]);
|
|
28158
29396
|
} else if (keepParent) {
|
|
28159
|
-
await run4("zip", ["-q", "-r", "-y", zipPath,
|
|
29397
|
+
await run4("zip", ["-q", "-r", "-y", zipPath, path48.basename(target)], { cwd: path48.dirname(target) });
|
|
28160
29398
|
} else {
|
|
28161
29399
|
await run4("zip", ["-q", "-r", "-y", zipPath, "."], { cwd: target });
|
|
28162
29400
|
}
|
|
@@ -28171,7 +29409,7 @@ async function packNativeBuild(input) {
|
|
|
28171
29409
|
}
|
|
28172
29410
|
|
|
28173
29411
|
// src/lib/native-upload.ts
|
|
28174
|
-
import
|
|
29412
|
+
import fs47 from "fs/promises";
|
|
28175
29413
|
var PART_CONCURRENCY = 2;
|
|
28176
29414
|
async function call(ctx, route, init, what, log) {
|
|
28177
29415
|
let res;
|
|
@@ -28217,7 +29455,7 @@ async function uploadNativeArchive(grant, zipPath, sizeBytes, log, onProgress) {
|
|
|
28217
29455
|
const base = grant.uploadUrl.replace(/\/+$/, "");
|
|
28218
29456
|
const encoded = grant.uploadPath.split("/").map(encodeURIComponent).join("/");
|
|
28219
29457
|
const auth = { Authorization: `Bearer ${grant.token}` };
|
|
28220
|
-
const file = await
|
|
29458
|
+
const file = await fs47.open(zipPath, "r");
|
|
28221
29459
|
try {
|
|
28222
29460
|
if (sizeBytes <= grant.limits.singlePutMaxBytes) {
|
|
28223
29461
|
const body = await readPart(file, 0, sizeBytes);
|
|
@@ -28396,10 +29634,10 @@ async function runPublish2(opts, log) {
|
|
|
28396
29634
|
const description = await describeNativeArchive(packed.zipPath, opts);
|
|
28397
29635
|
log.step(describeLine(description));
|
|
28398
29636
|
log.step("Hashing the archive\u2026");
|
|
28399
|
-
const { sha256:
|
|
28400
|
-
const version = buildVersion(opts.buildVersion, description.appVersion,
|
|
29637
|
+
const { sha256: sha2563, sizeBytes } = await hashFile(packed.zipPath);
|
|
29638
|
+
const version = buildVersion(opts.buildVersion, description.appVersion, sha2563);
|
|
28401
29639
|
const { platform, arch, entry, installedBytes, minOs, engine } = description;
|
|
28402
|
-
const grant = await requestNativeUpload(ctx, { platform, arch, sha256:
|
|
29640
|
+
const grant = await requestNativeUpload(ctx, { platform, arch, sha256: sha2563, sizeBytes }, log);
|
|
28403
29641
|
if (!grant) return false;
|
|
28404
29642
|
if (grant.uploaded) {
|
|
28405
29643
|
log.step(`Already uploaded (${sizeLine(sizeBytes)}).`);
|
|
@@ -28410,7 +29648,7 @@ async function runPublish2(opts, log) {
|
|
|
28410
29648
|
}
|
|
28411
29649
|
const build = await publishNativeBuild(
|
|
28412
29650
|
ctx,
|
|
28413
|
-
{ platform, arch, sha256:
|
|
29651
|
+
{ platform, arch, sha256: sha2563, sizeBytes, installedBytes, version, entry, ...minOs ? { minOs } : {}, ...engine ? { engine } : {} },
|
|
28414
29652
|
log
|
|
28415
29653
|
);
|
|
28416
29654
|
if (!build) return false;
|
|
@@ -28660,6 +29898,25 @@ ${c.bold("Usage")}
|
|
|
28660
29898
|
--resellable lets players resell the item to each
|
|
28661
29899
|
other on the market; durable items only, off by
|
|
28662
29900
|
default, and --no-resellable turns it back off.
|
|
29901
|
+
Store items: --price 0 is Free; --description,
|
|
29902
|
+
--tag <word> (repeat; "set <id> --clear-tags"
|
|
29903
|
+
empties them), --image <file.png|webp>
|
|
29904
|
+
(its transparent picture), --model <file.glb>,
|
|
29905
|
+
--package <dir> (files for other games' agents;
|
|
29906
|
+
.git, node_modules, .genex, secrets and links
|
|
29907
|
+
stay out) and --shared ("other games can use
|
|
29908
|
+
it"; durable only). With the store on, "add"
|
|
29909
|
+
makes a draft and --json prints it as one
|
|
29910
|
+
document (a refusal: one genex.item_error
|
|
29911
|
+
line, exit 1). "publish <id>" puts it on sale and
|
|
29912
|
+
"unpublish <id>" takes it off \u2014 in the Genex
|
|
29913
|
+
app the person presses Put on sale instead.
|
|
29914
|
+
genex items <sub> [words|id] Other creators' shared items in this game:
|
|
29915
|
+
search | add | remove | list. "add <id>" lets
|
|
29916
|
+
players use it here (free, not a purchase) and
|
|
29917
|
+
writes genex-items/<id>/ \u2014 item.json and the
|
|
29918
|
+
author's package \u2014 for your agent to integrate.
|
|
29919
|
+
It reaches the store with the next live build.
|
|
28663
29920
|
genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
|
|
28664
29921
|
--image <path|url> builds it FROM a reference
|
|
28665
29922
|
image instead (prompt optional). Quality knobs:
|
|
@@ -29044,6 +30301,10 @@ ${c.bold("Examples")}
|
|
|
29044
30301
|
genex shop set sku_123 --icon https://assets.genex.technology/generations/abc123/image-main
|
|
29045
30302
|
genex shop test sku_123
|
|
29046
30303
|
genex shop remove sku_123
|
|
30304
|
+
genex shop add "Frost Axe" --price 1200 --type durable --description "Freezes on hit." --tag axe --image .genex/scratch/axe.png --shared --json
|
|
30305
|
+
genex shop publish sku_123
|
|
30306
|
+
genex items search "wings" --tag fire
|
|
30307
|
+
genex items add sku_456
|
|
29047
30308
|
genex llm models
|
|
29048
30309
|
genex llm models --all
|
|
29049
30310
|
genex llm bench "Reply with one short taunt." --samples 5 --max-credits 20 --user-approved
|
|
@@ -29526,6 +30787,10 @@ function parseArgs(argv) {
|
|
|
29526
30787
|
"--operation-id",
|
|
29527
30788
|
"--price",
|
|
29528
30789
|
"--icon",
|
|
30790
|
+
// `genex shop add|set` and `genex items search`: one search word per --tag.
|
|
30791
|
+
"--tag",
|
|
30792
|
+
// `genex shop add|set --package <dir>`: the item's files for other games' agents.
|
|
30793
|
+
"--package",
|
|
29529
30794
|
"--rename",
|
|
29530
30795
|
"--type",
|
|
29531
30796
|
"--title",
|
|
@@ -29793,6 +31058,19 @@ function parseArgs(argv) {
|
|
|
29793
31058
|
case "--no-resellable":
|
|
29794
31059
|
parsed.options.resellable = false;
|
|
29795
31060
|
break;
|
|
31061
|
+
// `genex shop add|set`: "Other games can use it" (P10). A pair for the
|
|
31062
|
+
// same reason as --resellable: `set` must tell OFF from ABSENT.
|
|
31063
|
+
case "--shared":
|
|
31064
|
+
parsed.options.shared = true;
|
|
31065
|
+
break;
|
|
31066
|
+
case "--no-shared":
|
|
31067
|
+
parsed.options.shared = false;
|
|
31068
|
+
break;
|
|
31069
|
+
// `genex shop set <id> --clear-tags`: --tag replaces the list and an
|
|
31070
|
+
// empty --tag is refused, so emptying it needs its own flag.
|
|
31071
|
+
case "--clear-tags":
|
|
31072
|
+
parsed.options.clearTags = true;
|
|
31073
|
+
break;
|
|
29796
31074
|
case "--list":
|
|
29797
31075
|
parsed.options.list = true;
|
|
29798
31076
|
break;
|
|
@@ -29892,6 +31170,8 @@ function parseArgs(argv) {
|
|
|
29892
31170
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
29893
31171
|
return parsed;
|
|
29894
31172
|
}
|
|
31173
|
+
} else if (parsed.command === "items") {
|
|
31174
|
+
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
29895
31175
|
} else if (parsed.command === "domain") {
|
|
29896
31176
|
if (!parsed.options.hostname) parsed.options.hostname = arg;
|
|
29897
31177
|
else {
|
|
@@ -29931,7 +31211,9 @@ function parseArgs(argv) {
|
|
|
29931
31211
|
}
|
|
29932
31212
|
const imageCount = parsed.options.imageUrls?.length ?? 0;
|
|
29933
31213
|
if (imageCount > 0) {
|
|
29934
|
-
if (parsed.command === "
|
|
31214
|
+
if (parsed.command === "shop" && imageCount > 1) {
|
|
31215
|
+
parsed.error = "genex shop takes one --image: the item's picture.";
|
|
31216
|
+
} else if (parsed.command === "model" && imageCount > 1) {
|
|
29935
31217
|
parsed.error = "genex model accepts one --image reference. Use character --image for multiple character views.";
|
|
29936
31218
|
} else if (parsed.command === "character" || parsed.command === "creature") {
|
|
29937
31219
|
if (["preview", "finalize", "import", "animate", "motions", "adopt"].includes(parsed.options.name ?? "")) {
|
|
@@ -30181,8 +31463,8 @@ function applyValueFlag(options, flag, value) {
|
|
|
30181
31463
|
}
|
|
30182
31464
|
case "--price": {
|
|
30183
31465
|
const n = Number(value);
|
|
30184
|
-
if (!Number.isInteger(n) || n
|
|
30185
|
-
throw new Error(`Invalid --price value: ${value} (whole coin, e.g. 100)`);
|
|
31466
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < 0) {
|
|
31467
|
+
throw new Error(`Invalid --price value: ${value} (whole coin, e.g. 100; 0 is Free)`);
|
|
30186
31468
|
}
|
|
30187
31469
|
options.price = n;
|
|
30188
31470
|
break;
|
|
@@ -30190,6 +31472,16 @@ function applyValueFlag(options, flag, value) {
|
|
|
30190
31472
|
case "--icon":
|
|
30191
31473
|
options.icon = value;
|
|
30192
31474
|
break;
|
|
31475
|
+
// A forgotten value must not swallow the next flag (`--tag --json` would
|
|
31476
|
+
// tag the item "--json" and print no draft) or vanish (`--package ""`).
|
|
31477
|
+
case "--tag":
|
|
31478
|
+
if (!value.trim() || value.startsWith("--")) throw new Error("Missing value for --tag");
|
|
31479
|
+
(options.itemTags ??= []).push(value);
|
|
31480
|
+
break;
|
|
31481
|
+
case "--package":
|
|
31482
|
+
if (!value.trim() || value.startsWith("--")) throw new Error("Missing value for --package");
|
|
31483
|
+
options.packageDir = value;
|
|
31484
|
+
break;
|
|
30193
31485
|
case "--type": {
|
|
30194
31486
|
if (value !== "consumable" && value !== "durable" && !MODEL_RIG_TYPES.includes(value)) {
|
|
30195
31487
|
throw new Error(
|
|
@@ -30493,7 +31785,10 @@ async function main() {
|
|
|
30493
31785
|
await runMakeRemixable(parsed.options);
|
|
30494
31786
|
break;
|
|
30495
31787
|
case "shop":
|
|
30496
|
-
await runShop(parsed.options);
|
|
31788
|
+
await runShop({ ...parsed.options, picture: parsed.options.imageUrl, modelFile: parsed.options.modelId });
|
|
31789
|
+
break;
|
|
31790
|
+
case "items":
|
|
31791
|
+
await runItems(parsed.options);
|
|
30497
31792
|
break;
|
|
30498
31793
|
case "domain":
|
|
30499
31794
|
await runDomain(parsed.options);
|