@genex-ai/cli-demo 1.5.2-dev.398 → 1.6.0

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.
Files changed (26) hide show
  1. package/dist/index.js +288 -373
  2. package/package.json +1 -1
  3. package/templates/asset-viewer/asset.config.json +25 -0
  4. package/templates/asset-viewer/genex-asset.example.json +222 -0
  5. package/templates/asset-viewer/index.html +200 -0
  6. package/templates/asset-viewer/package.json +24 -0
  7. package/templates/asset-viewer/public/fonts/Geist-variable.woff2 +0 -0
  8. package/templates/asset-viewer/public/fonts/GeistMono-variable.woff2 +0 -0
  9. package/templates/asset-viewer/shared-files.sha256.json +14 -0
  10. package/templates/asset-viewer/src/asset/PLACEHOLDER.ts +237 -0
  11. package/templates/asset-viewer/src/main.js +149 -0
  12. package/templates/asset-viewer/src/viewer/gates-overlay.js +521 -0
  13. package/templates/asset-viewer/src/viewer/hud.js +73 -0
  14. package/templates/asset-viewer/src/viewer/stage.js +760 -0
  15. package/templates/asset-viewer/tools/emit-manifest.mjs +653 -0
  16. package/templates/asset-viewer/tools/gates.mjs +682 -0
  17. package/templates/asset-viewer/tools/stamp-manifest.mjs +134 -0
  18. package/templates/asset-viewer/vite.config.js +24 -0
  19. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  20. package/templates/skills/genex-ai-video/SKILL.md +1 -1
  21. package/templates/skills/genex-asset-author/SKILL.md +101 -0
  22. package/templates/skills/genex-game-director/references/routing-map.md +1 -1
  23. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  24. package/templates/skills/genex-threejs-visual-validation/SKILL.md +3 -7
  25. package/templates/skills/genex-updates/SKILL.md +1 -1
  26. package/templates/skills/genex-monetization/SKILL.md +0 -261
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- var RAW_CHANNEL = "dev";
11
+ var RAW_CHANNEL = "latest";
12
12
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
13
13
  var STANDS = {
14
14
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -1014,7 +1014,6 @@ Important note: put soul into your creations, with many details and love. Aim to
1014
1014
  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.)
1015
1015
  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.
1016
1016
  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.
1017
- 19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
1018
1017
  ${CONTRACT_END}
1019
1018
  `;
1020
1019
  var CLAUDE_IMPORT_LINE = "@AGENTS.md";
@@ -1238,7 +1237,7 @@ async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), ver
1238
1237
  if (await readSkillsMarker(skillsDir) === version) return false;
1239
1238
  const src = target.full ? templatesDir : path7.join(templatesDir, "skills");
1240
1239
  const dest = target.full ? target.baseDir : skillsDir;
1241
- await copyTemplates(src, dest, { exclude: ["controllers", "motion"] });
1240
+ await copyTemplates(src, dest, { exclude: ["controllers", "motion", "asset-viewer"] });
1242
1241
  await pruneRemovedSkills(skillsDir, log);
1243
1242
  await writeSkillsMarker(skillsDir, version);
1244
1243
  return true;
@@ -1722,7 +1721,7 @@ async function runInit(opts) {
1722
1721
  const dest = t.full ? t.baseDir : path10.join(t.baseDir, "skills");
1723
1722
  const { copied, updated } = await copyTemplates(src, dest, {
1724
1723
  force: opts.force,
1725
- exclude: ["controllers", "motion"]
1724
+ exclude: ["controllers", "motion", "asset-viewer"]
1726
1725
  });
1727
1726
  await pruneRemovedSkills(path10.join(t.baseDir, "skills"), log);
1728
1727
  await writeSkillsMarker(path10.join(t.baseDir, "skills"));
@@ -4964,307 +4963,6 @@ async function runMakeRemixable(opts) {
4964
4963
  }
4965
4964
  }
4966
4965
 
4967
- // src/commands/domain.ts
4968
- var SUBCOMMANDS = ["add", "list", "verify", "remove"];
4969
- function statusWord(d) {
4970
- if (d.status === "active") return c.green("live");
4971
- if (d.status === "failed") return c.red("stopped");
4972
- return c.dim("waiting for DNS");
4973
- }
4974
- async function runDomain(opts) {
4975
- const log = createLogger({ quiet: opts.quiet });
4976
- const sub = (opts.name ?? "list").trim();
4977
- if (!SUBCOMMANDS.includes(sub)) {
4978
- log.error(`Unknown subcommand \`${sub}\`. Use: ${SUBCOMMANDS.join(", ")}.`);
4979
- process.exitCode = 1;
4980
- return;
4981
- }
4982
- const needsHost = sub !== "list";
4983
- const hostname = opts.hostname?.trim();
4984
- if (needsHost && !hostname) {
4985
- log.error(`\`genex domain ${sub}\` needs a hostname, e.g. ${c.cyan(`genex domain ${sub} play.yourdomain.com`)}.`);
4986
- process.exitCode = 1;
4987
- return;
4988
- }
4989
- const meta = await readProject();
4990
- if (!meta?.id) {
4991
- log.error("This folder isn't linked to a game.");
4992
- log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
4993
- process.exitCode = 1;
4994
- return;
4995
- }
4996
- const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
4997
- let token = opts.token ?? await readUserToken(opts.envPath);
4998
- if (!token) {
4999
- if (opts.noAuth) {
5000
- log.error("Not signed in. Re-run without --no-auth to connect.");
5001
- process.exitCode = 1;
5002
- return;
5003
- }
5004
- log.plain("Not signed in \u2014 connecting\u2026");
5005
- try {
5006
- token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
5007
- log,
5008
- inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
5009
- });
5010
- } catch (err) {
5011
- if (err instanceof AuthPendingError) {
5012
- printAuthHandoff(log, err);
5013
- log.dim(`Then re-run ${c.cyan(`genex domain ${sub}`)}.`);
5014
- return;
5015
- }
5016
- log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
5017
- process.exitCode = 1;
5018
- return;
5019
- }
5020
- await writeUserToken(token, opts.envPath);
5021
- }
5022
- const base = `${apiUrl}/api/projects/${encodeURIComponent(meta.id)}/domains`;
5023
- const auth = { Authorization: `Bearer ${token}` };
5024
- const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, "Content-Type": "application/json", ...init2?.headers ?? {} } });
5025
- let res;
5026
- try {
5027
- if (sub === "list") res = await call(base);
5028
- else if (sub === "add") res = await call(base, { method: "POST", body: JSON.stringify({ hostname }) });
5029
- else if (sub === "verify")
5030
- res = await call(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
5031
- else res = await call(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
5032
- } catch (err) {
5033
- log.error(`Couldn't reach the API at ${apiUrl}.`);
5034
- log.dim(` ${String(err)}`);
5035
- process.exitCode = 1;
5036
- return;
5037
- }
5038
- if (!res.ok) {
5039
- if (await printedStructuredError(res)) {
5040
- process.exitCode = 1;
5041
- return;
5042
- }
5043
- const body = await res.json().catch(() => null);
5044
- if (res.status === 503) {
5045
- log.error("Custom domains aren't available on this Genex environment yet.");
5046
- } else if (res.status === 404) {
5047
- log.error(sub === "list" || sub === "add" ? "That game wasn't found on this account." : `${hostname} isn't connected to this game.`);
5048
- } else {
5049
- log.error(body?.message ?? body?.error ?? `Request failed (${res.status}).`);
5050
- }
5051
- process.exitCode = 1;
5052
- return;
5053
- }
5054
- const data = await res.json();
5055
- if (opts.json) {
5056
- log.plain(JSON.stringify(data, null, 2));
5057
- return;
5058
- }
5059
- if (sub === "list") {
5060
- const rows = data.domains ?? [];
5061
- if (rows.length === 0) {
5062
- log.plain("No domains connected to this game.");
5063
- log.dim(` ${c.cyan("genex domain add play.yourdomain.com")} to connect one.`);
5064
- return;
5065
- }
5066
- for (const d of rows) log.plain(` ${d.hostname.padEnd(34)} ${statusWord(d)}`);
5067
- return;
5068
- }
5069
- if (sub === "add") {
5070
- if (data.supported === false) {
5071
- log.plain(String(data.message ?? "That domain's DNS host doesn't support one-click setup."));
5072
- const manual = data.manual;
5073
- const records = Array.isArray(manual?.records) ? manual.records : [];
5074
- if (records.length > 0) {
5075
- log.plain("");
5076
- log.plain(" Add these two records at your DNS host:");
5077
- log.plain("");
5078
- for (const r of records) {
5079
- log.plain(` ${c.cyan(String(r.type ?? ""))} ${String(r.name ?? "")}`);
5080
- log.plain(` ${String(r.value ?? "")}`);
5081
- }
5082
- log.plain("");
5083
- if (manual?.apexHint) {
5084
- log.dim(" At a root domain your host may call the first one ALIAS or ANAME.");
5085
- }
5086
- log.dim(` Then run: genex domain verify ${String(data.hostname ?? "")}`);
5087
- log.dim(" DNS can take a few minutes to spread.");
5088
- return;
5089
- }
5090
- log.dim(" Your game stays reachable at its usual address.");
5091
- return;
5092
- }
5093
- const applyUrl = String(data.applyUrl ?? "");
5094
- const providerName = String(data.providerName ?? "your DNS host");
5095
- log.success(`${data.hostname} can be connected through ${providerName}.`);
5096
- log.plain("");
5097
- log.plain(` Approve the DNS change here: ${c.cyan(applyUrl)}`);
5098
- log.plain("");
5099
- log.dim(" One click writes the records. Nothing to copy or paste.");
5100
- if (!applyUrl.startsWith("https://")) {
5101
- log.warn("That DNS host returned a setup link we could not verify \u2014 not opening it.");
5102
- } else if (!opts.noOpen) {
5103
- openBrowser(applyUrl, () => log.dim(" (Couldn't open a browser \u2014 use the link above.)"));
5104
- }
5105
- log.dim(` Then ${c.cyan(`genex domain verify ${String(data.hostname)}`)} once you have approved it.`);
5106
- return;
5107
- }
5108
- if (sub === "verify") {
5109
- const status = String(data.status ?? "");
5110
- if (status === "active") log.success(`${hostname} is live.`);
5111
- else if (status === "failed") log.error(`${hostname} is stopped \u2014 its verification record is missing.`);
5112
- else log.plain(`${hostname} isn't verified yet \u2014 DNS changes can take a few minutes to spread.`);
5113
- return;
5114
- }
5115
- log.success(`${hostname} disconnected.`);
5116
- }
5117
-
5118
- // src/commands/shop.ts
5119
- var SUBS = ["list", "add", "set", "remove"];
5120
- function money(cents) {
5121
- return cents === void 0 ? "" : ` ($${(cents / 100).toFixed(2)})`;
5122
- }
5123
- async function runShop(opts) {
5124
- const log = createLogger({ quiet: opts.quiet });
5125
- const sub = opts.name?.trim() || "list";
5126
- if (!SUBS.includes(sub)) {
5127
- log.error(`Unknown subcommand ${c.cyan(sub)}. Use: ${SUBS.join(", ")}.`);
5128
- process.exitCode = 1;
5129
- return;
5130
- }
5131
- const meta = await readProject();
5132
- if (!meta?.id) {
5133
- log.error("This folder isn't linked to a game.");
5134
- log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
5135
- process.exitCode = 1;
5136
- return;
5137
- }
5138
- const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
5139
- let token = opts.token ?? await readUserToken(opts.envPath);
5140
- if (!token) {
5141
- if (opts.noAuth) {
5142
- log.error("Not signed in. Re-run without --no-auth to connect.");
5143
- process.exitCode = 1;
5144
- return;
5145
- }
5146
- log.plain("Not signed in \u2014 connecting\u2026");
5147
- try {
5148
- token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
5149
- log,
5150
- inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
5151
- });
5152
- } catch (err) {
5153
- if (err instanceof AuthPendingError) {
5154
- printAuthHandoff(log, err);
5155
- log.dim(`Then re-run ${c.cyan(`genex shop ${sub}`)}.`);
5156
- return;
5157
- }
5158
- log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
5159
- process.exitCode = 1;
5160
- return;
5161
- }
5162
- await writeUserToken(token, opts.envPath);
5163
- }
5164
- const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
5165
- const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, ...init2?.headers ?? {} } });
5166
- const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
5167
- async function fail3(res2, what) {
5168
- if (printedStructuredError(res2)) {
5169
- process.exitCode = 1;
5170
- return;
5171
- }
5172
- process.exitCode = 1;
5173
- const body = await res2.json().catch(() => ({}));
5174
- if (res2.status === 404 && body.error === "not_found") {
5175
- log.error("In-game purchases aren't enabled on this environment yet.");
5176
- return;
5177
- }
5178
- log.error(body.message ?? `Couldn't ${what} (HTTP ${res2.status}).`);
5179
- if (body.error === "price_off_grid") {
5180
- log.dim(` See the full list any time: ${c.cyan("genex shop list")}`);
5181
- }
5182
- }
5183
- if (sub === "list") {
5184
- const res2 = await call(projectSkus);
5185
- if (!res2.ok) return fail3(res2, "read your shop");
5186
- const body = await res2.json();
5187
- if (opts.json) {
5188
- log.plain(JSON.stringify(body, null, 2));
5189
- return;
5190
- }
5191
- if (body.items.length === 0) {
5192
- log.plain("This game sells nothing yet.");
5193
- log.dim(` Add something: ${c.cyan('genex shop add "Iron Key" --price 100')}`);
5194
- log.dim(` Prices available: ${body.priceGrid.join(", ")} coin`);
5195
- return;
5196
- }
5197
- log.plain(`${body.items.length} item${body.items.length === 1 ? "" : "s"}:`);
5198
- for (const s of body.items) {
5199
- const state = s.active ? "" : c.dim(" (retired)");
5200
- log.plain(
5201
- ` ${c.cyan(s.id)} ${s.name} \u2014 ${s.priceCoins} coin${money(s.priceDisplayUsdCents)} ${c.dim(s.type)}${state}`
5202
- );
5203
- }
5204
- log.dim(`Prices available: ${body.priceGrid.join(", ")} coin`);
5205
- return;
5206
- }
5207
- if (sub === "add") {
5208
- const name = opts.hostname?.trim();
5209
- if (!name) {
5210
- log.error(`\`genex shop add\` needs a name, e.g. ${c.cyan('genex shop add "Iron Key" --price 100')}.`);
5211
- process.exitCode = 1;
5212
- return;
5213
- }
5214
- if (!opts.price) {
5215
- log.error(`\`genex shop add\` needs ${c.cyan("--price")}, in coin.`);
5216
- process.exitCode = 1;
5217
- return;
5218
- }
5219
- const res2 = await call(projectSkus, {
5220
- method: "POST",
5221
- body: JSON.stringify({
5222
- name,
5223
- priceCoins: opts.price,
5224
- type: opts.type === "durable" ? "durable" : "consumable",
5225
- ...opts.icon ? { iconUrl: opts.icon } : {}
5226
- })
5227
- });
5228
- if (!res2.ok) return fail3(res2, "add that item");
5229
- const sku = await res2.json();
5230
- if (opts.json) {
5231
- log.plain(JSON.stringify(sku, null, 2));
5232
- return;
5233
- }
5234
- log.success(`Added ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
5235
- log.plain(` id: ${c.cyan(sku.id)}`);
5236
- log.dim(` Use it in the game: buy({ skuId: "${sku.id}" })`);
5237
- return;
5238
- }
5239
- const skuId = opts.hostname?.trim();
5240
- if (!skuId) {
5241
- log.error(`\`genex shop ${sub}\` needs an item id \u2014 see ${c.cyan("genex shop list")}.`);
5242
- process.exitCode = 1;
5243
- return;
5244
- }
5245
- const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
5246
- if (sub === "set") {
5247
- const patch = {};
5248
- if (opts.rename) patch.name = opts.rename;
5249
- if (opts.price) patch.priceCoins = opts.price;
5250
- if (opts.icon) patch.iconUrl = opts.icon;
5251
- if (Object.keys(patch).length === 0) {
5252
- log.error(`Nothing to change. Pass ${c.cyan("--rename")}, ${c.cyan("--price")} or ${c.cyan("--icon")}.`);
5253
- process.exitCode = 1;
5254
- return;
5255
- }
5256
- const res2 = await call(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
5257
- if (!res2.ok) return fail3(res2, "update that item");
5258
- const sku = await res2.json();
5259
- log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
5260
- return;
5261
- }
5262
- const res = await call(skuUrl, { method: "DELETE" });
5263
- if (!res.ok) return fail3(res, "remove that item");
5264
- log.success("Removed from the shop.");
5265
- log.dim(" Players who already bought it keep it.");
5266
- }
5267
-
5268
4966
  // src/lib/promote.ts
5269
4967
  async function promoteBuild(apiUrl, projectId, token, log) {
5270
4968
  let res;
@@ -17564,22 +17262,22 @@ async function context2(opts) {
17564
17262
  const project = await readProject();
17565
17263
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
17566
17264
  }
17567
- async function readVideo(path22, log) {
17265
+ async function readVideo(path23, log) {
17568
17266
  let bytes;
17569
17267
  try {
17570
- bytes = await readFile(path22);
17268
+ bytes = await readFile(path23);
17571
17269
  } catch {
17572
- log.error(`Can't read ${path22}.`);
17270
+ log.error(`Can't read ${path23}.`);
17573
17271
  return null;
17574
17272
  }
17575
17273
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
17576
- log.error(`${basename(path22)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
17274
+ log.error(`${basename(path23)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
17577
17275
  return null;
17578
17276
  }
17579
17277
  return bytes;
17580
17278
  }
17581
- async function uploadVideo(apiUrl, token, characterId, path22, bytes, log) {
17582
- const contentType = /\.mov$/i.test(path22) ? "video/quicktime" : "video/mp4";
17279
+ async function uploadVideo(apiUrl, token, characterId, path23, bytes, log) {
17280
+ const contentType = /\.mov$/i.test(path23) ? "video/quicktime" : "video/mp4";
17583
17281
  const minted = await apiFetch(
17584
17282
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
17585
17283
  {
@@ -17594,7 +17292,7 @@ async function uploadVideo(apiUrl, token, characterId, path22, bytes, log) {
17594
17292
  return null;
17595
17293
  }
17596
17294
  const { uploadUrl, videoUrl } = await minted.json();
17597
- log.dim(` uploading ${basename(path22)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
17295
+ log.dim(` uploading ${basename(path23)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
17598
17296
  const put = await fetch(uploadUrl, {
17599
17297
  method: "PUT",
17600
17298
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -19452,6 +19150,241 @@ async function runMotion(opts) {
19452
19150
  }
19453
19151
  }
19454
19152
 
19153
+ // src/commands/asset-new.ts
19154
+ import fs23 from "fs";
19155
+ import fsp from "fs/promises";
19156
+ import path22 from "path";
19157
+ import { pathToFileURL } from "url";
19158
+ var EXTRA_FILES = [
19159
+ "genex-asset.example.json",
19160
+ "public/fonts/Geist-variable.woff2",
19161
+ "public/fonts/GeistMono-variable.woff2"
19162
+ ];
19163
+ var HOLDER_PLACEHOLDER = "Your Name";
19164
+ function parseDims(input) {
19165
+ const parts = String(input).toLowerCase().split("x").map((p) => Number(p.trim()));
19166
+ if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n) || n <= 0)) {
19167
+ throw new Error(`--dims must look like 0.36x0.36x0.70 (width x depth x height), got: ${input}`);
19168
+ }
19169
+ const [width, depth, height] = parts;
19170
+ return [width, height, depth];
19171
+ }
19172
+ function parseBand(input) {
19173
+ const parts = String(input).split("-").map((p) => Number(p.replace(/[_,\s]/g, "")));
19174
+ const [lo, hi] = parts;
19175
+ if (parts.length !== 2 || lo === void 0 || hi === void 0 || !Number.isInteger(lo) || !Number.isInteger(hi) || lo <= 0 || lo >= hi) {
19176
+ throw new Error(`--tri-band must look like 800-2000, got: ${input}`);
19177
+ }
19178
+ return [lo, hi];
19179
+ }
19180
+ function titleCase(slug) {
19181
+ return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
19182
+ }
19183
+ function seedAssetSource(placeholder, opts) {
19184
+ const pascal = opts.pascalCase(opts.slug);
19185
+ let out = placeholder.replaceAll("Placeholder", pascal);
19186
+ const line = (pattern, replacement) => {
19187
+ if (!pattern.test(out)) {
19188
+ throw new Error(`the template's PLACEHOLDER.ts no longer carries the scaffold line ${pattern}`);
19189
+ }
19190
+ out = out.replace(pattern, replacement);
19191
+ };
19192
+ line(/^\/\/ .* - procedural Three\.js asset\.$/m, `// ${opts.name} - procedural Three.js asset.`);
19193
+ line(
19194
+ /^\/\/ MIT License - Copyright \(c\) \d+ .*$/m,
19195
+ `// MIT License - Copyright (c) ${opts.year} ${opts.holder}. Free to use, modify and ship.`
19196
+ );
19197
+ line(/^const SLUG = .*$/m, `const SLUG = '${opts.slug}';`);
19198
+ line(/^const NAME = .*$/m, `const NAME = '${opts.name.replaceAll("'", "\\'")}';`);
19199
+ line(
19200
+ /^const SIZE_METERS: \[number, number, number\] = .*$/m,
19201
+ `const SIZE_METERS: [number, number, number] = [${opts.sizeMeters.join(", ")}];`
19202
+ );
19203
+ line(/^const TARGET_TRIANGLES = .*$/m, `const TARGET_TRIANGLES = ${opts.targetTriangles};`);
19204
+ return out;
19205
+ }
19206
+ function fillTemplate(text, values) {
19207
+ return text.replace(/\{\{(\w+)\}\}/g, (_, key) => {
19208
+ const value = values[key];
19209
+ if (value === void 0) throw new Error(`unfilled placeholder: {{${key}}}`);
19210
+ return value;
19211
+ });
19212
+ }
19213
+ var GITIGNORE = ["node_modules/", "dist/", ".gates/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n");
19214
+ function designDoc(opts) {
19215
+ return `# ${opts.name}
19216
+
19217
+ A free, MIT-licensed procedural Three.js asset for the Genex asset library.
19218
+ One file, one factory, no dependency but \`three\`.
19219
+
19220
+ - **slug:** \`${opts.slug}\`
19221
+ - **size:** ${opts.sizeMeters.map((n) => n.toFixed(2)).join(" \xD7 ")} m (x \xD7 y \xD7 z, y = height)
19222
+ - **triangle band:** ${opts.triBand[0].toLocaleString("en-US")} - ${opts.triBand[1].toLocaleString("en-US")}
19223
+ - **origin:** base centre, y = 0. **Units:** metres. **Facing:** Y-up, +Z forward.
19224
+ - **licence:** MIT, copyright ${opts.holder}. Publishing this asset releases it MIT \u2014
19225
+ the dashboard's Copy code button makes it copyable by anyone.
19226
+
19227
+ ## Boundary note
19228
+
19229
+ This asset is **code**, not generated art: the lane is procedural geometry
19230
+ authored in TypeScript. No player character applies \u2014 the project is a viewer,
19231
+ not a game.
19232
+
19233
+ ## Build plan & status
19234
+
19235
+ 1. [ ] blockout - silhouette and real dimensions
19236
+ 2. [ ] form-refinement - the details that make it read as manufactured
19237
+ 3. [ ] material-pass - MeshStandardMaterial + DataTexture only
19238
+ 4. [ ] interaction-pass - \`assetRuntime.nodes\`, \`dispose()\`, options
19239
+
19240
+ **Now:** milestone 1.
19241
+
19242
+ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-build\`.
19243
+
19244
+ ## Decisions
19245
+ `;
19246
+ }
19247
+ async function runAssetNew(options) {
19248
+ const log = createLogger();
19249
+ const cwd = options.dir ? path22.resolve(options.dir) : process.cwd();
19250
+ const slug = options.assetSlug;
19251
+ if (!slug) {
19252
+ log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
19253
+ return 1;
19254
+ }
19255
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
19256
+ log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
19257
+ return 1;
19258
+ }
19259
+ const templateDir = path22.join(getTemplatesDir(), "asset-viewer");
19260
+ if (!fs23.existsSync(templateDir)) {
19261
+ log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
19262
+ return 1;
19263
+ }
19264
+ const manifestTools = await import(pathToFileURL(path22.join(templateDir, "tools", "emit-manifest.mjs")).href);
19265
+ const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
19266
+ const lockPath = path22.join(templateDir, "shared-files.sha256.json");
19267
+ const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
19268
+ const actual = hashSharedFiles(templateDir);
19269
+ const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
19270
+ if (drifted.length) {
19271
+ log.error(`The bundled viewer template is damaged (${drifted.join(", ")}) \u2014 reinstall the CLI.`);
19272
+ return 1;
19273
+ }
19274
+ const name = options.title ?? titleCase(slug);
19275
+ const sizeMeters = parseDims(options.dims ?? "1x1x1");
19276
+ const triBand = parseBand(options.triBand ?? "500-8000");
19277
+ const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
19278
+ const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
19279
+ const outDir = path22.resolve(cwd, options.out ?? slug);
19280
+ if (fs23.existsSync(outDir) && fs23.readdirSync(outDir).length > 0 && !options.force) {
19281
+ log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
19282
+ return 1;
19283
+ }
19284
+ const version = "1.0.0";
19285
+ const targetTriangles = Math.round((triBand[0] + triBand[1]) / 2);
19286
+ const config = {
19287
+ slug,
19288
+ name,
19289
+ summary: options.summary ?? `${name} - a free procedural Three.js asset, ${sizeMeters.map((n) => n.toFixed(2)).join(" \xD7 ")} m.`,
19290
+ tags: (options.tags ?? "prop").split(",").map((t) => t.trim()).filter(Boolean),
19291
+ materialClass: "unclassified",
19292
+ geometryClass: "unclassified",
19293
+ difficulty: "easy",
19294
+ version,
19295
+ minThreeRevision: 160,
19296
+ statedSizeMeters: sizeMeters,
19297
+ triBand,
19298
+ drawCallMax: 8,
19299
+ materialMax: 4,
19300
+ textureBytesMax: 4194304,
19301
+ maxTextureDimension: 1024,
19302
+ silhouetteCollapseRatio: 0.15,
19303
+ license: { holder, year },
19304
+ viewer: { cameraDistanceMul: 1 }
19305
+ };
19306
+ await fsp.mkdir(outDir, { recursive: true });
19307
+ for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
19308
+ const to = path22.join(outDir, rel);
19309
+ await fsp.mkdir(path22.dirname(to), { recursive: true });
19310
+ await fsp.copyFile(path22.join(templateDir, rel), to);
19311
+ }
19312
+ const pkg = fillTemplate(await fsp.readFile(path22.join(templateDir, "package.json"), "utf8"), {
19313
+ slug,
19314
+ name,
19315
+ version
19316
+ });
19317
+ await fsp.writeFile(path22.join(outDir, "package.json"), pkg, "utf8");
19318
+ await fsp.writeFile(path22.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
19319
+ await fsp.writeFile(path22.join(outDir, ".gitignore"), GITIGNORE, "utf8");
19320
+ await fsp.writeFile(
19321
+ path22.join(outDir, "DESIGN.md"),
19322
+ designDoc({ name, slug, sizeMeters, triBand, holder }),
19323
+ "utf8"
19324
+ );
19325
+ const placeholder = await fsp.readFile(path22.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
19326
+ const seeded = seedAssetSource(placeholder, {
19327
+ slug,
19328
+ name,
19329
+ sizeMeters,
19330
+ targetTriangles,
19331
+ holder,
19332
+ year,
19333
+ pascalCase
19334
+ });
19335
+ const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
19336
+ await fsp.mkdir(path22.join(outDir, "src", "asset"), { recursive: true });
19337
+ await fsp.writeFile(path22.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
19338
+ const copied = hashSharedFiles(outDir);
19339
+ const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
19340
+ if (mismatched.length) {
19341
+ log.error(`Internal error: copy was not verbatim for ${mismatched.join(", ")}`);
19342
+ return 1;
19343
+ }
19344
+ await fsp.writeFile(
19345
+ path22.join(outDir, PARITY_FILENAME),
19346
+ JSON.stringify(
19347
+ {
19348
+ note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
19349
+ templateVersion: lock.templateVersion,
19350
+ scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString(),
19351
+ files: copied
19352
+ },
19353
+ null,
19354
+ 2
19355
+ ) + "\n",
19356
+ "utf8"
19357
+ );
19358
+ const lines = [
19359
+ "",
19360
+ ` ${c.bold(name)} \u2192 ${outDir}`,
19361
+ ` ${sizeMeters.map((n) => n.toFixed(2)).join(" \xD7 ")} m \xB7 ${triBand[0].toLocaleString("en-US")} - ${triBand[1].toLocaleString("en-US")} tris \xB7 viewer ${lock.templateVersion}`,
19362
+ ""
19363
+ ];
19364
+ if (holder === HOLDER_PLACEHOLDER) {
19365
+ lines.push(
19366
+ ` ${c.yellow("\u26A0")} No --holder given: the MIT copyright line says "${HOLDER_PLACEHOLDER}".`,
19367
+ ` Set the real name in asset.config.json (license.holder) AND line 2 of src/asset/${assetFilename}`,
19368
+ ` before publishing \u2014 published assets are copyable by anyone under that name.`,
19369
+ ""
19370
+ );
19371
+ }
19372
+ lines.push(
19373
+ " next:",
19374
+ ` cd ${outDir}`,
19375
+ " npm install",
19376
+ " npx playwright-core install chromium # once per machine",
19377
+ ` # author src/asset/${assetFilename} \u2014 the genex-asset-author skill carries the contract`,
19378
+ " npm run verify # build + 17 gates + manifest stamp",
19379
+ " npx genex init # create the project (sign-in if needed)",
19380
+ " npx genex preview --no-build # draft turntable to show the user",
19381
+ " npx genex publish --no-build --categories assets",
19382
+ ""
19383
+ );
19384
+ log.plain(lines.join("\n"));
19385
+ return 0;
19386
+ }
19387
+
19455
19388
  // src/index.ts
19456
19389
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
19457
19390
  var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
@@ -19481,15 +19414,6 @@ ${c.bold("Usage")}
19481
19414
  genex publish [options] Build + push + make live, then list it in the gallery.
19482
19415
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
19483
19416
  private source onto a public managed genex repo.
19484
- genex domain <sub> [host] Play this game on a domain you own:
19485
- add | list | verify | remove. "add" opens a
19486
- one-click approval at your DNS host \u2014 no
19487
- records to copy or paste.
19488
- genex shop <sub> [name|id] What this game sells: list | add | set | remove.
19489
- "add" prints the item id your game passes to
19490
- buy({ skuId }) \u2014 the platform owns the catalog,
19491
- so this is the only way one exists. Prices come
19492
- off a fixed grid, printed on any refusal.
19493
19417
  genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
19494
19418
  genex sfx "<prompt>" [options] Generate a sound effect (mp3); prints a public asset URL.
19495
19419
  genex music "<prompt>" [options] Generate an instrumental music track (mp3); prints a
@@ -19564,6 +19488,13 @@ ${c.bold("Usage")}
19564
19488
  extract | masks | text-color | trim | audit.
19565
19489
  Pure local work (no API); the PNG tools read
19566
19490
  a file or URL, audit scans the project.
19491
+ genex asset new <slug> Scaffold ONE asset project for the free MIT
19492
+ asset library (its own project \u2014 one asset,
19493
+ one turntable page). Only when the user asks
19494
+ to make or publish an asset \u2014 never offered
19495
+ unprompted. Options: --title --dims WxDxH
19496
+ --tri-band LOW-HIGH --holder "<your name>"
19497
+ --summary --tags --out --force.
19567
19498
 
19568
19499
  ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `image` `video`)")}
19569
19500
  --terrain (texture) seamless tiling surface for terrain/ground.
@@ -19730,12 +19661,6 @@ ${c.bold("Examples")}
19730
19661
  genex publish
19731
19662
  genex publish --categories games,vfx
19732
19663
  genex make-remixable
19733
- genex domain add play.mygame.com
19734
- genex domain list
19735
- genex shop list
19736
- genex shop add "Iron Key" --price 100 --type durable
19737
- genex shop set sku_123 --price 200
19738
- genex shop remove sku_123
19739
19664
  genex publish --no-push --title "My Game"
19740
19665
  genex model "weathered wooden barrel with iron bands"
19741
19666
  genex sfx "punchy laser zap" --duration 2
@@ -19787,10 +19712,6 @@ function parseArgs(argv) {
19787
19712
  "--name",
19788
19713
  "--repo",
19789
19714
  "--remixed-from",
19790
- "--price",
19791
- "--icon",
19792
- "--rename",
19793
- "--type",
19794
19715
  "--title",
19795
19716
  "--description",
19796
19717
  "--categories",
@@ -19844,7 +19765,13 @@ function parseArgs(argv) {
19844
19765
  "--config",
19845
19766
  "--set",
19846
19767
  "--speed",
19847
- "--video"
19768
+ "--video",
19769
+ // `genex asset new` value flags.
19770
+ "--dims",
19771
+ "--tri-band",
19772
+ "--holder",
19773
+ "--summary",
19774
+ "--tags"
19848
19775
  ]);
19849
19776
  let i = 0;
19850
19777
  while (i < argv.length) {
@@ -19862,9 +19789,6 @@ function parseArgs(argv) {
19862
19789
  case "--no-auth":
19863
19790
  parsed.options.noAuth = true;
19864
19791
  break;
19865
- case "--no-open":
19866
- parsed.options.noOpen = true;
19867
- break;
19868
19792
  case "--no-push":
19869
19793
  parsed.options.noPush = true;
19870
19794
  break;
@@ -19999,20 +19923,15 @@ function parseArgs(argv) {
19999
19923
  } else {
20000
19924
  (parsed.options.selectors ??= []).push(arg);
20001
19925
  }
20002
- } else if (parsed.command === "shop") {
20003
- if (!parsed.options.hostname) parsed.options.hostname = arg;
20004
- else {
20005
- parsed.error = `Unexpected argument: ${arg}`;
20006
- return parsed;
20007
- }
20008
- } else if (parsed.command === "domain") {
20009
- if (!parsed.options.hostname) parsed.options.hostname = arg;
20010
- else {
19926
+ } else if (parsed.command === "animations") {
19927
+ parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
19928
+ } else if (parsed.command === "asset") {
19929
+ if (parsed.options.name === "new" && parsed.options.assetSlug === void 0) {
19930
+ parsed.options.assetSlug = arg;
19931
+ } else {
20011
19932
  parsed.error = `Unexpected argument: ${arg}`;
20012
19933
  return parsed;
20013
19934
  }
20014
- } else if (parsed.command === "animations") {
20015
- parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
20016
19935
  } else {
20017
19936
  parsed.error = `Unexpected argument: ${arg}`;
20018
19937
  return parsed;
@@ -20149,6 +20068,21 @@ function applyValueFlag(options, flag, value) {
20149
20068
  case "--set":
20150
20069
  options.set = value;
20151
20070
  break;
20071
+ case "--dims":
20072
+ options.dims = value;
20073
+ break;
20074
+ case "--tri-band":
20075
+ options.triBand = value;
20076
+ break;
20077
+ case "--holder":
20078
+ options.holder = value;
20079
+ break;
20080
+ case "--summary":
20081
+ options.summary = value;
20082
+ break;
20083
+ case "--tags":
20084
+ options.tags = value;
20085
+ break;
20152
20086
  case "--speed": {
20153
20087
  const n = Number(value);
20154
20088
  if (!Number.isFinite(n) || n <= 0) {
@@ -20181,27 +20115,6 @@ function applyValueFlag(options, flag, value) {
20181
20115
  options.duration = n;
20182
20116
  break;
20183
20117
  }
20184
- case "--price": {
20185
- const n = Number(value);
20186
- if (!Number.isInteger(n) || n <= 0) {
20187
- throw new Error(`Invalid --price value: ${value} (whole coin, e.g. 100)`);
20188
- }
20189
- options.price = n;
20190
- break;
20191
- }
20192
- case "--icon":
20193
- options.icon = value;
20194
- break;
20195
- case "--type": {
20196
- if (value !== "consumable" && value !== "durable") {
20197
- throw new Error(`Invalid --type value: ${value} (consumable or durable)`);
20198
- }
20199
- options.type = value;
20200
- break;
20201
- }
20202
- case "--rename":
20203
- options.rename = value;
20204
- break;
20205
20118
  case "--aspect":
20206
20119
  options.aspect = value;
20207
20120
  break;
@@ -20382,12 +20295,6 @@ async function main() {
20382
20295
  case "make-remixable":
20383
20296
  await runMakeRemixable(parsed.options);
20384
20297
  break;
20385
- case "shop":
20386
- await runShop(parsed.options);
20387
- break;
20388
- case "domain":
20389
- await runDomain(parsed.options);
20390
- break;
20391
20298
  case "controller":
20392
20299
  await runController({ ...parsed.options, kind: parsed.options.name });
20393
20300
  break;
@@ -20435,6 +20342,14 @@ async function main() {
20435
20342
  await runAnimationsSearch(parsed.options);
20436
20343
  }
20437
20344
  break;
20345
+ case "asset":
20346
+ if (parsed.options.name !== "new") {
20347
+ log.error("Unknown asset command. Use `genex asset new <slug>`.");
20348
+ process.exitCode = 1;
20349
+ } else {
20350
+ process.exitCode = await runAssetNew(parsed.options);
20351
+ }
20352
+ break;
20438
20353
  case "preview":
20439
20354
  await runPreview(parsed.options);
20440
20355
  break;