@genex-ai/cli-demo 1.34.0-dev.693 → 1.34.1-dev.694

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