@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 byName = new Map(entries.map((e) => [e.name.normalize("NFC").toLowerCase(), e]));
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 = byName.get(parts.slice(0, j).join("/").normalize("NFC").toLowerCase());
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 collect = (chunk) => {
7236
+ const collect2 = (chunk) => {
7236
7237
  tail = (tail + chunk.toString()).slice(-8e3);
7237
7238
  };
7238
- child.stdout.on("data", collect);
7239
- child.stderr.on("data", collect);
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/commands/shop.ts
8557
- var SUBS = ["list", "add", "set", "remove", "test"];
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(`genex shop ${sub}`)}.`);
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
- const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
8612
- async function fail5(res2, what) {
8613
- if (printedStructuredError(res2)) {
8614
- process.exitCode = 1;
8615
- return;
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
- process.exitCode = 1;
8618
- const body = await res2.json().catch(() => ({}));
8619
- if (res2.status === 404 && body.error === "not_found") {
8620
- log.error("In-game purchases aren't enabled on this environment yet.");
8621
- return;
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
- log.error(body.message ?? `Couldn't ${what} (HTTP ${res2.status}).`);
8624
- if (body.error === "price_off_grid") {
8625
- log.dim(` See the full list any time: ${c.cyan("genex shop list")}`);
8940
+ if (!portableName(relPath)) {
8941
+ out.skipped.push({ path: relPath, reason: PackSkip.UnsafeName });
8942
+ continue;
8626
8943
  }
8627
- if (body.error === "not_resellable_type") {
8628
- log.dim(` Add it with ${c.cyan("--type durable")} \u2014 a consumable is used up, so there is nothing left to resell.`);
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
- if (sub === "list") {
8632
- const res2 = await call2(projectSkus);
8633
- if (!res2.ok) return fail5(res2, "read your shop");
8634
- const body = await res2.json();
8635
- if (opts.json) {
8636
- log.plain(JSON.stringify(body, null, 2));
8637
- return;
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(` Prices available: ${body.priceGrid.join(", ")} coin`);
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 state = s.active ? "" : c.dim(" (retired)");
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(`Prices available: ${body.priceGrid.join(", ")} coin`);
8654
- return;
9523
+ log.dim(prices);
8655
9524
  }
8656
- if (sub === "add") {
9525
+ async add(attachments) {
9526
+ const { log, opts } = this;
8657
9527
  const name = opts.hostname?.trim();
8658
- if (!name) {
8659
- log.error(`\`genex shop add\` needs a name, e.g. ${c.cyan('genex shop add "Iron Key" --price 100')}.`);
8660
- process.exitCode = 1;
8661
- return;
8662
- }
8663
- if (!opts.price) {
8664
- log.error(`\`genex shop add\` needs ${c.cyan("--price")}, in coin.`);
8665
- process.exitCode = 1;
8666
- return;
8667
- }
8668
- const res2 = await call2(projectSkus, {
8669
- method: "POST",
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 (!res2.ok) return fail5(res2, "add that item");
8679
- const sku = await res2.json();
8680
- if (opts.json) {
8681
- log.plain(JSON.stringify(sku, null, 2));
8682
- return;
8683
- }
8684
- log.success(`Added ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
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
- return;
8689
- }
8690
- const skuId = opts.hostname?.trim();
8691
- if (!skuId) {
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
- const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
8697
- if (sub === "test") {
8698
- const res2 = await call2(`${skuUrl}/test-grant`, { method: "POST" });
8699
- if (!res2.ok) return fail5(res2, "grant yourself a test copy");
8700
- const got = await res2.json();
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
- if (sub === "set") {
8709
- const patch = {};
8710
- if (opts.rename) patch.name = opts.rename;
8711
- if (opts.price) patch.priceCoins = opts.price;
8712
- if (opts.icon) patch.iconUrl = opts.icon;
8713
- if (opts.resellable !== void 0) patch.resellable = opts.resellable;
8714
- if (Object.keys(patch).length === 0) {
8715
- log.error(
8716
- `Nothing to change. Pass ${c.cyan("--rename")}, ${c.cyan("--price")}, ${c.cyan("--icon")} or ${c.cyan("--resellable")}.`
8717
- );
8718
- process.exitCode = 1;
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 res2 = await call2(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
8722
- if (!res2.ok) return fail5(res2, "update that item");
8723
- const sku = await res2.json();
8724
- log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
8725
- if (opts.resellable === false) log.dim(" Listings already on the market stay up.");
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
- const res = await call2(skuUrl, { method: "DELETE" });
8729
- if (!res.ok) return fail5(res, "remove that item");
8730
- log.success("Removed from the shop.");
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 fs24 from "fs/promises";
9221
- import path24 from "path";
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 fs24.stat(file);
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 fs24.readFile(file);
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 ${path24.basename(file)} as the cover\u2026`);
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 || path24.resolve(scratch) === path24.resolve(file)) {
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 fs24.readFile(scratch).catch(() => null);
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 invalid = (message) => finishClip({ kind: "invalid", message }, file, opts, log);
9278
- if (!clip) return invalid("--commit goes with --clip <file>: pass the recording to send.");
9279
- if (opts.imagePath?.trim()) return invalid("Pass either an image (the cover) or --clip <file> (the cover clip), not both.");
9280
- if (!opts.commit?.trim()) return invalid("--clip needs --commit <sha>: the git commit of the build the recording shows.");
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 fs24.stat(clip);
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 invalid(`${clip} is ${(stat.size / 1024 / 1024).toFixed(1)} MB; a cover clip recording can be at most ${CLIP_MAX_BYTES / 1024 / 1024} MB.`);
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 fs24.readFile(clip);
10526
+ bytes = await fs27.readFile(clip);
9289
10527
  } catch {
9290
- return invalid(`Can't read ${clip} \u2014 pass the path of an MP4 or WebM recording.`);
10528
+ return invalid2(`Can't read ${clip} \u2014 pass the path of an MP4 or WebM recording.`);
9291
10529
  }
9292
- log.step(`Uploading ${path24.basename(clip)} as the cover clip\u2026`);
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 fs27 from "fs/promises";
9351
- import path27 from "path";
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 fs26 from "fs/promises";
9539
- import path26 from "path";
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 fs25 from "fs/promises";
9544
- import path25 from "path";
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) => path25.join(cwd, ".genex", "asset-budget.json");
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 fs25.readFile(storePath(cwd), "utf8");
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 fs25.access(path25.join(cwd, ".genex"));
9594
- await fs25.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
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 fs25.stat(storePath(cwd));
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 = path26.join(cwd, ".genex", "generation-admission.lock");
9710
- await fs26.mkdir(path26.dirname(lock), { recursive: true });
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 fs26.open(lock, "wx");
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 fs26.unlink(lock);
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 = createHash("sha256").update(JSON.stringify([input.apiUrl, createPath, input.body])).digest("hex");
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 fs27.readFile(filePath);
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[path27.extname(filePath).toLowerCase()] ?? "image/png";
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: ${path27.basename(ref).slice(0, 120)}`;
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 fs27.readFile(opts.inpaintUrl);
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 fs27.mkdir(outDir, { recursive: true });
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 = path27.join(outDir, `glass-${i + 1}.png`);
10432
- await fs27.writeFile(outPath, PNG4.sync.write(r.png));
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 fs28 from "fs/promises";
11063
- import path28 from "path";
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 ${path28.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
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 fs28.readFile(filePath);
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(`${path28.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.`);
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(`${path28.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
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: path28.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
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 ${path28.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
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 path29 from "path";
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 = path29.join(path29.relative(process.cwd(), cwd) || ".", local.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 fs30 from "fs/promises";
11577
- import path31 from "path";
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 createHash2 } from "crypto";
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 createHash2("sha256").update(JSON.stringify(definition)).digest("hex");
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 fs29 from "fs/promises";
20422
- import path30 from "path";
20423
- var ANIMS_DEST = path30.join("public", "assets", "anims");
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 = path30.join(root, "src", "controllers", "character");
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 = path30.join(root, ANIMS_DEST);
20449
- const gameManifestPath = path30.join(destDir, "manifest.json");
21686
+ const destDir = path32.join(root, ANIMS_DEST);
21687
+ const gameManifestPath = path32.join(destDir, "manifest.json");
20450
21688
  if (opts.reset) {
20451
- await fs29.rm(destDir, { recursive: true, force: true });
20452
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path30.sep)} (--reset)`);
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 byName = /* @__PURE__ */ new Map();
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
- byName.set(entry.name, entry);
21724
+ byName2.set(entry.name, entry);
20487
21725
  }
20488
21726
  }
20489
- const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
20490
- const cacheDir = path30.join(
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 fs29.mkdir(cacheDir, { recursive: true });
20495
- await fs29.mkdir(destDir, { recursive: true });
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 = path30.join(destDir, entry.file);
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 = path30.join(cacheDir, entry.file);
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 fs29.writeFile(cached, buf);
21751
+ await fs32.writeFile(cached, buf);
20514
21752
  }
20515
- await fs29.copyFile(cached, dest);
21753
+ await fs32.copyFile(cached, dest);
20516
21754
  installedCount++;
20517
21755
  addedBytes += entry.bytes;
20518
- log.dim(` ${path30.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
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 fs29.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
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 + path30.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
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 = path30.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
20573
- const manifest = JSON.parse(await fs29.readFile(snapshotPath, "utf8"));
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 byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
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 = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
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(), ...byName.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 fs29.readFile(file, "utf8"));
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 fs29.stat(file)).size === bytes;
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 fs29.access(p);
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 = path31.join("src", "controllers");
20907
- var ASSETS_DEST = path31.join("public", "assets");
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 = path31.join(getTemplatesDir(), "controllers");
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 + path31.sep)}`);
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: path31.join(CODE_DEST, 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: path31.join(ASSETS_DEST, path31.basename(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 = path31.join(root, file.rel);
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 fs30.mkdir(path31.dirname(dest), { recursive: true });
20948
- await fs30.copyFile(path31.join(srcDir, file.from), dest);
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(path31.join(root, ASSETS_DEST, "meshy-character.json"))) {
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 = path31.join(args.root, ASSETS_DEST, "meshy-character.json");
21034
- await fs30.mkdir(path31.dirname(destination), { recursive: true });
21035
- await fs30.writeFile(
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 = path31.join(root, ASSETS_DEST, "avatar.vrm");
21181
- await fs30.mkdir(path31.dirname(dest), { recursive: true });
21182
- await fs30.copyFile(path31.join(srcDir, "assets", "default-avatar.vrm"), dest);
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 fs30.access(p);
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 fs31 from "fs/promises";
21196
- import path32 from "path";
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 = path32.join("public", "assets", "meshy-character.json");
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 fs31.readFile(file, "utf8");
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 ${path32.basename(filePath)} as a rigged character`,
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(path47, log) {
23198
+ async function readVideo(path49, log) {
21961
23199
  let bytes;
21962
23200
  try {
21963
- bytes = await readFile(path47);
23201
+ bytes = await readFile(path49);
21964
23202
  } catch {
21965
- log.error(`Can't read ${path47}.`);
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(path47)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
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, path47, bytes, log) {
21975
- const contentType = /\.mov$/i.test(path47) ? "video/quicktime" : "video/mp4";
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(path47)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
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 fs32 from "fs/promises";
22323
- import path33 from "path";
23560
+ import fs35 from "fs/promises";
23561
+ import path35 from "path";
22324
23562
 
22325
23563
  // src/lib/motion/npz.ts
22326
- import zlib from "zlib";
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 ? zlib.inflateRawSync(raw) : Buffer.from(raw);
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 fs32.readFile(opts.constraintsPath, "utf8");
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 fs32.stat(sel).catch(() => null);
24836
+ const st = await fs35.stat(sel).catch(() => null);
23599
24837
  if (st?.isDirectory()) {
23600
- const names2 = await fs32.readdir(sel);
23601
- for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(path33.join(sel, n));
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 fs32.readFile(opts.gatesPath, "utf8")));
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 = path33.basename(file).replace(/\.npz$/, "");
24896
+ const stem = path35.basename(file).replace(/\.npz$/, "");
23659
24897
  try {
23660
- reports.push(analyzeTake(stem, await fs32.readFile(file), gates));
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 fs32.readFile(opts.configPath, "utf8"));
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 = path33.basename(file).replace(/\.npz$/, "");
24954
+ const stem = path35.basename(file).replace(/\.npz$/, "");
23717
24955
  try {
23718
- inputs.push({ stem, take: loadTake(await fs32.readFile(file)) });
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 ?? path33.basename(opts.out).replace(/\.json$/, "");
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 fs32.mkdir(path33.dirname(path33.resolve(opts.out)), { recursive: true });
24978
+ await fs35.mkdir(path35.dirname(path35.resolve(opts.out)), { recursive: true });
23741
24979
  const json = JSON.stringify(result.data);
23742
- await fs32.writeFile(opts.out, json);
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 = path33.join("src", "motion");
24998
+ var MOTION_DEST = path35.join("src", "motion");
23761
24999
  async function motionInstall(opts, log) {
23762
- const srcDir = path33.join(getTemplatesDir(), "motion");
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 + path33.sep)}`);
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 = path33.join(root, MOTION_DEST, rel);
23778
- const exists5 = await fs32.access(dest).then(() => true, () => false);
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 ${path33.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
25019
+ log.dim(` skipped ${path35.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
23782
25020
  continue;
23783
25021
  }
23784
- await fs32.mkdir(path33.dirname(dest), { recursive: true });
23785
- await fs32.copyFile(path33.join(srcDir, rel), dest);
25022
+ await fs35.mkdir(path35.dirname(dest), { recursive: true });
25023
+ await fs35.copyFile(path35.join(srcDir, rel), dest);
23786
25024
  copied++;
23787
- log.dim(` ${path33.join(MOTION_DEST, rel)}`);
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 fs32.writeFile(out, JSON.stringify(doc));
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 fs33 from "fs/promises";
23866
- import path34 from "path";
23867
- var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
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 fs33.mkdir(dir, { recursive: true });
23871
- const p = path34.join(dir, name);
23872
- await fs33.writeFile(p, Buffer.from(b64, "base64"));
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 || !SUBS2.includes(sub)) {
23885
- log.error(`Usage: genex blender <${SUBS2.join("|")}>`);
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 = path34.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
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 ?? path34.join(outDir, "scene.glb");
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 fs33.mkdir(path34.dirname(target), { recursive: true });
24003
- await fs33.writeFile(target, Buffer.from(r.glbBase64, "base64"));
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 fs33.readFile(opts.input, "utf8");
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 = path34.basename(opts.input);
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 fs34 from "fs";
25398
+ import fs37 from "fs";
24161
25399
  import fsp from "fs/promises";
24162
- import path35 from "path";
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 ? path35.resolve(options.dir) : process.cwd();
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 = path35.join(getTemplatesDir(), "asset-viewer");
24267
- if (!fs34.existsSync(templateDir)) {
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(path35.join(templateDir, "tools", "emit-manifest.mjs")).href);
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 = path35.join(templateDir, "shared-files.sha256.json");
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 = path35.resolve(cwd, options.out ?? slug);
24287
- if (fs34.existsSync(outDir) && fs34.readdirSync(outDir).length > 0 && !options.force) {
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 = path35.join(outDir, rel);
24316
- await fsp.mkdir(path35.dirname(to), { recursive: true });
24317
- await fsp.copyFile(path35.join(templateDir, rel), to);
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(path35.join(templateDir, "package.json"), "utf8"), {
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(path35.join(outDir, "package.json"), pkg, "utf8");
24325
- await fsp.writeFile(path35.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
24326
- await fsp.writeFile(path35.join(outDir, ".gitignore"), GITIGNORE, "utf8");
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
- path35.join(outDir, "DESIGN.md"),
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(path35.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
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(path35.join(outDir, "src", "asset"), { recursive: true });
24344
- await fsp.writeFile(path35.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
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
- path35.join(outDir, PARITY_FILENAME),
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 path38 from "path";
25634
+ import path40 from "path";
24397
25635
 
24398
25636
  // src/commands/doctor.ts
24399
- import path37 from "path";
25637
+ import path39 from "path";
24400
25638
 
24401
25639
  // src/commands/llm.ts
24402
- import fs35 from "fs/promises";
24403
- import path36 from "path";
25640
+ import fs38 from "fs/promises";
25641
+ import path38 from "path";
24404
25642
  import { randomUUID as randomUUID2 } from "crypto";
24405
- var SUBS3 = ["models", "bench", "price", "status", "cancel"];
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 (!SUBS3.includes(sub)) {
24552
- fail5(`llm ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS3.join(", ")}.`);
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 fs35.readFile(path36.resolve(cwd, opts.schemaPath), "utf8"));
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 = path36.join(cwd, LLM_BENCH_FILE);
25307
- await fs35.mkdir(path36.dirname(file), { recursive: true });
25308
- await fs35.writeFile(file, JSON.stringify(record, null, 2) + "\n");
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 money2 = rowMoney(row);
25416
- const reserved = money2.reserved === null ? "reserved \u2014" : `${money2.reserved} ${money2.unit} 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 money2 = rowMoney(row);
25492
- log.dim(` ${money2.charged === null ? "charge unknown" : `${money2.charged} ${money2.unit} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
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 fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
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(path37.join(target.baseDir, "skills"));
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 = path38.join(t.baseDir, "skills");
27243
+ const dest = path40.join(t.baseDir, "skills");
26006
27244
  let copied;
26007
27245
  let updated;
26008
27246
  try {
26009
- ({ copied, updated } = await copyTemplates(path38.join(templatesDir, "skills"), dest, {
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 fs36 from "fs/promises";
26082
- import path39 from "path";
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 fs36.access(path39.join(cwd, ".genex"));
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 fs41 from "fs";
27477
+ import fs44 from "fs";
26240
27478
  import fsp2 from "fs/promises";
26241
27479
  import os12 from "os";
26242
- import path45 from "path";
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 path40 from "path";
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() : path40.join(home, ".genex");
26255
- const logDir = path40.join(dir, "logs");
27492
+ const dir = home === os8.homedir() ? getGenexDir() : path42.join(home, ".genex");
27493
+ const logDir = path42.join(dir, "logs");
26256
27494
  return {
26257
- credential: path40.join(dir, "player.json"),
26258
- state: path40.join(dir, "player-state.json"),
26259
- relaunch: path40.join(dir, "player-relaunch.json"),
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: path40.join(logDir, "player.log"),
26262
- errorLog: path40.join(logDir, "player.error.log")
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 fs37 from "fs/promises";
26268
- import path41 from "path";
27505
+ import fs40 from "fs/promises";
27506
+ import path43 from "path";
26269
27507
  async function readPlayerInstall(home) {
26270
27508
  try {
26271
- const raw = await fs37.readFile(playerPaths(home).credential, "utf8");
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 fs37.readFile(playerPaths(home).state, "utf8"));
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 fs37.mkdir(path41.dirname(file), { recursive: true, mode: 448 });
26310
- await fs37.writeFile(file, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
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 fs37.stat(file).then(() => true, () => false);
26319
- await fs37.rm(file, { force: true }).catch(() => {
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(path47, init = {}, provider) {
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}${path47}`, {
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 fs38 from "fs/promises";
27767
+ import fs41 from "fs/promises";
26530
27768
  import fsSync from "fs";
26531
27769
  import os9 from "os";
26532
- import path42 from "path";
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 = path42.join(dir, candidate);
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 = path42.join(os9.tmpdir(), `genex-player-${randomUUID3()}`);
26755
- await fs38.mkdir(cwd, { recursive: true, mode: 448 });
26756
- const outFile = path42.join(cwd, "result.out");
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 = path42.join(cwd, "schema.json");
26764
- await fs38.writeFile(schemaFile, JSON.stringify(job.schema), { mode: 384 });
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 fs38.readFile(outFile, "utf8").catch(() => "");
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 fs38.rm(cwd, { recursive: true, force: true }).catch(() => {
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 fs39 from "fs/promises";
28081
+ import fs42 from "fs/promises";
26844
28082
  import os10 from "os";
26845
- import path43 from "path";
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: path43.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
26863
- if (platform === "linux") return { log, errorLog, unit: path43.join(home, ".config", "systemd", "user", SYSTEMD_UNIT) };
26864
- return { log, errorLog, unit: path43.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "genex-player.cmd") };
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) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[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 fs39.mkdir(path43.dirname(paths.unit), { recursive: true });
26944
- await fs39.mkdir(path43.dirname(paths.log), { recursive: true, mode: 448 });
26945
- await fs39.writeFile(paths.unit, unitFile(platform, input, paths), { mode: platform === "win32" ? 420 : 384 });
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 fs39.stat(paths.unit).then(() => paths.unit, () => null);
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 fs39.rm(paths.unit, { force: true }).catch(() => {
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 fs40 from "fs/promises";
28248
+ import fs43 from "fs/promises";
27011
28249
  import fsSync2 from "fs";
27012
- import path44 from "path";
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 fs40.readFile(playerPaths(home).relaunch, "utf8");
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 fs40.mkdir(path44.dirname(file), { recursive: true, mode: 448 });
27051
- await fs40.writeFile(file, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), version }) + "\n", { mode: 384 });
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 fs40.mkdir(paths.logDir, { recursive: true, mode: 448 });
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 SUBS4 = ["install", "run", "status", "stop", "uninstall"];
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 (!SUBS4.includes(sub)) {
27421
- fail4(opts, log, `player ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS4.join(", ")}.`);
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 = fs41.openSync(paths.log, "a", 384);
27557
- err = fs41.openSync(paths.errorLog, "a", 384);
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
- fs41.closeSync(fd);
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} (${path45.basename(service.unit)})`);
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 createHash3 } from "crypto";
29023
+ import { createHash as createHash4 } from "crypto";
27786
29024
  import { createReadStream } from "fs";
27787
- import fs43 from "fs/promises";
29025
+ import fs46 from "fs/promises";
27788
29026
  import os13 from "os";
27789
- import path46 from "path";
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 fs42 from "fs/promises";
27795
- import zlib2 from "zlib";
27796
- var EOCD_SIGNATURE = 101010256;
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 CENTRAL_SIGNATURE = 33639248;
27800
- var LOCAL_SIGNATURE = 67324752;
29037
+ var CENTRAL_SIGNATURE2 = 33639248;
29038
+ var LOCAL_SIGNATURE2 = 67324752;
27801
29039
  var EOCD_MIN = 22;
27802
- var COMMENT_MAX = 65535;
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 UNIX_HOST = 3;
29045
+ var UNIX_HOST2 = 3;
27808
29046
  var INPUT_MIN = 64 * 1024;
27809
29047
  var INPUT_MAX = 1024 * 1024;
27810
- var S_IFMT = 61440;
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 & S_IFMT) === S_IFLNK;
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 findDirectory(file, size) {
27820
- const tailLength = Math.min(size, EOCD_MIN + COMMENT_MAX);
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) === EOCD_SIGNATURE) {
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 fs42.open(filePath, "r");
29104
+ const file = await fs45.open(filePath, "r");
27867
29105
  try {
27868
29106
  const { size } = await file.stat();
27869
- const directory = await findDirectory(file, size);
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) !== CENTRAL_SIGNATURE) throw new Error("broken zip directory entry");
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 === UNIX_HOST ? external >>> 16 : null
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 fs42.open(filePath, "r");
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) !== LOCAL_SIGNATURE) throw new Error(`broken zip entry ${entry.name}`);
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 = zlib2.createInflateRaw();
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 VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
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(path46.posix.basename(e.name)));
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) => path46.posix.basename(name.replace(/\/$/, ""));
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 ${path46.basename(zipPath)} as a zip: ${err.message}`);
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, sha2562) {
29353
+ function buildVersion(asked, appVersion, sha2563) {
28116
29354
  if (asked !== void 0) {
28117
- if (asked.length > VERSION_MAX || !VERSION.test(asked)) {
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 = sha2562.slice(0, VERSION_SHA_CHARS);
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 = createHash3("sha256");
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 fs43.readdir(dir, { withFileTypes: true });
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: path46.join(dir, apps[0].name), keepParent: true };
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 fs43.stat(input).catch(() => null);
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 fs43.mkdtemp(path46.join(os13.tmpdir(), "genex-native-"));
28153
- const zipPath = path46.join(dir, `${path46.basename(target).replace(/[^A-Za-z0-9._-]+/g, "-") || "build"}.zip`);
28154
- const cleanup = () => fs43.rm(dir, { recursive: true, force: true });
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, path46.basename(target)], { cwd: path46.dirname(target) });
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 fs44 from "fs/promises";
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 fs44.open(zipPath, "r");
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: sha2562, sizeBytes } = await hashFile(packed.zipPath);
28400
- const version = buildVersion(opts.buildVersion, description.appVersion, sha2562);
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: sha2562, sizeBytes }, log);
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: sha2562, sizeBytes, installedBytes, version, entry, ...minOs ? { minOs } : {}, ...engine ? { engine } : {} },
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 === "model" && imageCount > 1) {
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 <= 0) {
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);