@genex-ai/cli-demo 1.25.1-dev.618 → 1.26.0-dev.620

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.
@@ -7,7 +7,7 @@ import {
7
7
  isRenderMode,
8
8
  sceneSummary,
9
9
  sheetOf
10
- } from "./chunk-HEDRWUVP.js";
10
+ } from "./chunk-57CT5CAN.js";
11
11
  import {
12
12
  getCliVersion
13
13
  } from "./chunk-HYCSNWYX.js";
@@ -903,7 +903,6 @@ export {
903
903
  apiFetch,
904
904
  fetchSignedInEmail,
905
905
  restrictFilePermissions,
906
- getWorkspacePath,
907
906
  readWorkspace,
908
907
  writeWorkspace,
909
908
  writeUserToken,
package/dist/index.js CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  cloneSource,
15
15
  fetchCloneGrant,
16
16
  fetchSignedInEmail,
17
- getWorkspacePath,
18
17
  inHostedSession,
19
18
  isRenderMode,
20
19
  isStale,
@@ -42,7 +41,7 @@ import {
42
41
  writeProject,
43
42
  writeUserToken,
44
43
  writeWorkspace
45
- } from "./chunk-HEDRWUVP.js";
44
+ } from "./chunk-57CT5CAN.js";
46
45
  import {
47
46
  CLI_CHANNEL,
48
47
  DEFAULT_API_URL,
@@ -747,18 +746,28 @@ Important note: put soul into your creations, with many details and love. Aim to
747
746
  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.
748
747
  ${CONTRACT_END}
749
748
  `;
750
- var GENEX_TOOLS_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
751
- # Genex Tools (always in effect in this folder)
749
+ var TOOLS_CONTRACT_HEAD = `# Genex Tools (always in effect in this folder)
752
750
 
753
- This folder uses **Genex Tools**: AI asset generation for games, from your terminal. All via \`npx genex \u2026\` \u2014 \`model "<prompt>"\` (plus \`model --image <file>\` from a reference photo, \`model segment <id>\` into named parts, \`model rig <id>\` across 7 body plans, \`model animate <rig-id> --preset walk\`) \xB7 \`image\` (\`--transparent\` \xB7 \`--edit\` \xB7 \`--inpaint\` \xB7 \`--remove-bg\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 \`texture\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 rigged \`character\` / \`creature\` and \`character animate <id> "<verb>"\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`doctor\`. Full options with costs: \`npx genex --help\`. Each tool has a skill card (\`genex-tool-\u2026\`) \u2014 load the one for the lane you are using. The project and code are the user's own \u2014 Genex generates the assets, and can put the finished game live on the web when they want it.
754
-
755
- 1. **NEVER delete, empty, move, rename, or overwrite anything you did not create yourself.** This is someone's real project: it holds their code, their reference images, their notes, an earlier attempt \u2014 files that may exist nowhere else and have no undo, no trash, no backup. A non-empty folder is NORMAL and is never something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck prompt, and every tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files"). You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes.
751
+ This folder uses **Genex Tools**: AI asset generation for games, from your terminal. All via \`npx genex \u2026\` \u2014 \`model "<prompt>"\` (plus \`model --image <file>\` from a reference photo, \`model segment <id>\` into named parts, \`model rig <id>\` across 7 body plans, \`model animate <rig-id> --preset walk\`) \xB7 \`image\` (\`--transparent\` \xB7 \`--edit\` \xB7 \`--inpaint\` \xB7 \`--remove-bg\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 \`texture\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 rigged \`character\` / \`creature\` and \`character animate <id> "<verb>"\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`doctor\`. Full options with costs: \`npx genex --help\`. Each tool has a skill card (\`genex-tool-\u2026\`) \u2014 load the one for the lane you are using. The project and code are the user's own \u2014 Genex generates the assets, and can put the finished game live on the web when they want it.`;
752
+ var TOOLS_CONTRACT_LAWS_1_TO_4 = `1. **NEVER delete, empty, move, rename, or overwrite anything you did not create yourself.** This is someone's real project: it holds their code, their reference images, their notes, an earlier attempt \u2014 files that may exist nowhere else and have no undo, no trash, no backup. A non-empty folder is NORMAL and is never something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck prompt, and every tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files"). You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes.
756
753
  2. **Generated assets are local files the user owns.** Every generation downloads into \`./assets/\` and the command prints the local path \u2014 wire THAT path into the code, exactly like an asset they drew themselves. The URL printed beside it is provenance, not hosting: never fetch it at runtime, never ship a build that depends on it. Assets are theirs to move, rename, commit, or process further.
757
754
  3. **Run \`npx genex doctor\` when a command fails, and before promising what you can generate.** It reports sign-in, credit balance, and which generation lanes are actually live on this account \u2014 the only live source for all three. Treat every genex warning line as work, not noise, and never re-run a generation command to "retry" one that is still running: that bills a second asset. Attach to the first with \`npx genex wait <id>\`.
758
- 4. **Real assets beat placeholders.** When anything you build will be seen or heard by a player, reach for the belt first \u2014 a generated model instead of a colored box, a texture instead of a flat material, music and sound instead of silence, a rigged character instead of a capsule. Put soul into the details; never limit your imagination to what is easy to code. You know the tools and their costs (\`npx genex --help\`) \u2014 decide and generate on your own judgment, without stopping to ask permission; a status line saying what you queued is enough. Queue with \`--no-wait\` and keep building while generations run.
759
- 5. **When the game is built and playable \u2014 never mid-build \u2014 offer once, in one line: "Want to make your game live on the web with its own URL?"** Offer only if the game runs in the browser and builds to static files; otherwise never bring it up. A no means drop it for good. On a yes, run \`npx genex init --convert\` and follow what it prints \u2014 it turns this folder into a hosted Genex game workspace: the code stays the user's own, the source goes to a private repo, and the game gets a draft page plus a public URL when they choose to publish.
755
+ 4. **Real assets beat placeholders.** When anything you build will be seen or heard by a player, reach for the belt first \u2014 a generated model instead of a colored box, a texture instead of a flat material, music and sound instead of silence, a rigged character instead of a capsule. Put soul into the details; never limit your imagination to what is easy to code. You know the tools and their costs (\`npx genex --help\`) \u2014 decide and generate on your own judgment, without stopping to ask permission; a status line saying what you queued is enough. Queue with \`--no-wait\` and keep building while generations run.`;
756
+ var TOOLS_CONTRACT_OFFER = `5. **When the game is built and playable \u2014 never mid-build \u2014 offer once, in one line: "Want to make your game live on the web with its own URL?"** Offer only if the game runs in the browser and builds to static files; otherwise never bring it up. A no means drop it for good. On a yes, run \`npx genex init --convert\` and follow what it prints \u2014 it connects this folder to a hosted Genex game: the code and this toolkit stay exactly as they are, the source goes to a private repo, and the game gets a draft page plus a public URL when they choose to publish.`;
757
+ var TOOLS_CONTRACT_HOSTED_LAWS = `5. **This game is hosted on Genex now, and publishing is glue \u2014 never a rebuild.** The game ships exactly as the user built it: \`initEmbed()\` first in the boot code (the \`genex-threejs-embed-auth\` card has the call; the renderer draws before any \`await waitForPlayer()\`), a static build into \`dist/\` with relative asset paths, then \`npx genex preview\`. Never restructure, reformat or "improve" the game to ship it, never scaffold a new app around it, and never start a design document or a build plan for it \u2014 the user's own process is theirs. Load the \`genex-tool-publish\` card before the first preview; it owns the vocabulary, the links and the limits.
758
+ 6. **Ship first, then report.** \`preview\` prints a preflight \u2014 phone memory, a missing volume slider, an asset nobody wired, a viewport line. It never blocks a deploy, so push the build as it is, hand over the link, THEN relay each preflight line to the user in one plain sentence with an offer to fix it, and fix one only on their yes. The preflight is a report for the user, not a to-do list for you.
759
+ 7. **The link is the game's page, and there are two versions.** After every preview give the user \`<dashboard>/draft/<slug>\` \u2014 \`dashboardOrigins[0]\` and \`slug\` from \`.genex/project.json\` \u2014 never localhost, a file path or the bare play origin. \`preview\` updates the draft and never touches what players are on; the first release is \`npx genex publish\` (it lists the game); after that "publish it", "update it" and "yes" all mean \`npx genex promote\` \u2014 the exact draft build, no rebuild. Ask once per round of work, in one line, and keep working while you wait.`;
760
+ function toolsContractBlock(opts) {
761
+ return `${CONTRACT_BEGIN}
762
+ ${TOOLS_CONTRACT_HEAD}${opts.hosted ? " This game is also hosted on Genex: `preview` \xB7 `promote` \xB7 `publish` \xB7 `rollback` \xB7 `rename` work here (the `genex-tool-publish` card owns them)." : ""}
763
+
764
+ ${TOOLS_CONTRACT_LAWS_1_TO_4}
765
+ ${opts.hosted ? TOOLS_CONTRACT_HOSTED_LAWS : TOOLS_CONTRACT_OFFER}
760
766
  ${CONTRACT_END}
761
767
  `;
768
+ }
769
+ var GENEX_TOOLS_CONTRACT_BLOCK = toolsContractBlock({ hosted: false });
770
+ var GENEX_TOOLS_HOSTED_CONTRACT_BLOCK = toolsContractBlock({ hosted: true });
762
771
  var CLAUDE_IMPORT_LINE = "@AGENTS.md";
763
772
  var CONTRACT_REGION = /<!-- genex:contract:begin[^\n]*-->[\s\S]*?<!-- genex:contract:end -->/;
764
773
  function mergeContractBlock(existing, contractBlock = GENEX_CONTRACT_BLOCK) {
@@ -814,8 +823,8 @@ ${CLAUDE_IMPORT_LINE}
814
823
  }
815
824
  return changed;
816
825
  }
817
- async function writeToolsContract(projectDir) {
818
- return writeAgentsContract(projectDir, GENEX_TOOLS_CONTRACT_BLOCK);
826
+ async function writeToolsContract(projectDir, opts = {}) {
827
+ return writeAgentsContract(projectDir, toolsContractBlock({ hosted: opts.hosted === true }));
819
828
  }
820
829
  async function healAncestorContracts(projectDir, stopDir = os3.homedir()) {
821
830
  const findings = [];
@@ -894,21 +903,23 @@ import path4 from "path";
894
903
  // src/lib/workspace.ts
895
904
  async function resolveWorkspace(cwd = process.cwd()) {
896
905
  const project = await readProject(cwd);
897
- if (project) {
898
- const slug = typeof project.slug === "string" && project.slug.trim() ? project.slug.trim() : null;
899
- return { mode: "game", slug };
900
- }
906
+ const slug = project && typeof project.slug === "string" && project.slug.trim() ? project.slug.trim() : null;
901
907
  const ws = await readWorkspace(cwd);
902
- return { mode: ws?.mode === "tools" ? "tools" : null, slug: null };
908
+ if (ws?.mode === "tools") return { mode: "tools", slug, hosted: Boolean(project) };
909
+ if (project) return { mode: "game", slug, hosted: true };
910
+ return { mode: null, slug: null, hosted: false };
903
911
  }
904
912
  async function workspaceMode(cwd = process.cwd()) {
905
913
  return (await resolveWorkspace(cwd)).mode;
906
914
  }
907
915
  function workspaceHeaderLabel(ws) {
916
+ if (ws.hosted) return ws.slug ? { game: ws.slug } : ws.mode === "tools" ? "tools" : null;
908
917
  if (ws.mode === "tools") return "tools";
909
- if (ws.mode === "game" && ws.slug) return { game: ws.slug };
910
918
  return null;
911
919
  }
920
+ function platformRefusedIn(ws, command) {
921
+ return ws.mode === "tools" && !ws.hosted && refusedInTools(command);
922
+ }
912
923
  var REFUSED_IN_TOOLS = /* @__PURE__ */ new Set([
913
924
  "link",
914
925
  "pull",
@@ -982,19 +993,12 @@ async function hasGenexSkills(skillsDir) {
982
993
  }
983
994
  }
984
995
  var TOOL_SKILL_PREFIX = "genex-tool-";
985
- var TOOL_SKILLS = [
986
- "genex-tool-audio",
987
- "genex-tool-character",
988
- "genex-tool-image",
989
- "genex-tool-model",
990
- "genex-tool-texture",
991
- "genex-tool-video",
992
- "genex-tool-workflow"
993
- ];
996
+ var HOSTED_TOOL_SKILLS = ["genex-tool-publish", "genex-threejs-embed-auth"];
994
997
  function skillFamilyOf(name) {
995
998
  return name.startsWith(TOOL_SKILL_PREFIX) ? "tools" : "game";
996
999
  }
997
- function skillFamilyFilter(mode) {
1000
+ function skillFamilyFilter(mode, opts = {}) {
1001
+ const hostedExtras = new Set(HOSTED_TOOL_SKILLS);
998
1002
  return (rel) => {
999
1003
  const segments = rel.split(/[\\/]/);
1000
1004
  const top = segments[0] ?? "";
@@ -1003,6 +1007,7 @@ function skillFamilyFilter(mode) {
1003
1007
  }
1004
1008
  const skill = segments[1];
1005
1009
  if (skill === void 0) return true;
1010
+ if (mode === "tools" && hostedExtras.has(skill)) return opts.hosted === true;
1006
1011
  return skillFamilyOf(skill) === mode;
1007
1012
  };
1008
1013
  }
@@ -1057,38 +1062,13 @@ async function pruneRemovedSkills(skillsDir, log) {
1057
1062
  }
1058
1063
  return removed.length > 0;
1059
1064
  }
1060
- async function pruneToolCards(log, overrides) {
1061
- let removedAny = false;
1062
- for (const target of resolveAgentTargets(overrides ?? {})) {
1063
- const skillsDir = path4.join(target.baseDir, "skills");
1064
- for (const name of TOOL_SKILLS) {
1065
- const dir = path4.join(skillsDir, name);
1066
- try {
1067
- await fs4.access(dir);
1068
- } catch {
1069
- continue;
1070
- }
1071
- try {
1072
- await fs4.rm(dir, { recursive: true });
1073
- removedAny = true;
1074
- } catch {
1075
- }
1076
- }
1077
- }
1078
- if (removedAny) {
1079
- log?.plain(
1080
- "\u{1F9F9} Removed the Genex Tools cards \u2014 this is a game workspace now, and the game skills cover every lane."
1081
- );
1082
- }
1083
- return removedAny;
1084
- }
1085
- async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log, family = "game") {
1065
+ async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log, family = "game", hosted = false) {
1086
1066
  const skillsDir = path4.join(target.baseDir, "skills");
1087
1067
  if (!await hasGenexSkills(skillsDir)) return false;
1088
1068
  if (await readSkillsMarker(skillsDir) === version) return false;
1089
1069
  const src = target.full ? templatesDir : path4.join(templatesDir, "skills");
1090
1070
  const dest = target.full ? target.baseDir : skillsDir;
1091
- const familyFilter = skillFamilyFilter(family);
1071
+ const familyFilter = skillFamilyFilter(family, { hosted });
1092
1072
  const filter = target.full ? familyFilter : (rel) => familyFilter(`skills/${rel}`);
1093
1073
  await copyTemplates(src, dest, {
1094
1074
  exclude: ["controllers", "motion", "asset-viewer", "blender-service"],
@@ -1142,17 +1122,18 @@ async function cleanupLegacyGlobalSkills(log) {
1142
1122
  return false;
1143
1123
  }
1144
1124
  }
1145
- async function syncSkills(log, mode) {
1125
+ async function syncSkills(log, workspace) {
1146
1126
  try {
1147
1127
  await cleanupLegacyGlobalSkills(log);
1148
1128
  const version = getCliVersion();
1149
1129
  const templatesDir = getTemplatesDir();
1150
- const resolved = mode !== void 0 ? mode : await workspaceMode();
1130
+ const ws = workspace !== void 0 ? workspace : await resolveWorkspace();
1131
+ const resolved = ws.mode;
1151
1132
  const family = resolved === "tools" ? "tools" : "game";
1152
1133
  let refreshed = false;
1153
1134
  for (const target of resolveAgentTargets()) {
1154
1135
  try {
1155
- refreshed = await syncSkillsForTarget(target, templatesDir, version, log, family) || refreshed;
1136
+ refreshed = await syncSkillsForTarget(target, templatesDir, version, log, family, ws.hosted) || refreshed;
1156
1137
  } catch {
1157
1138
  }
1158
1139
  }
@@ -1163,7 +1144,7 @@ async function syncSkills(log, mode) {
1163
1144
  log.plain("\u{1F504} Genex build contract refreshed (AGENTS.md managed block)");
1164
1145
  }
1165
1146
  } else if (resolved === "tools") {
1166
- if (await writeToolsContract(process.cwd())) {
1147
+ if (await writeToolsContract(process.cwd(), { hosted: ws.hosted })) {
1167
1148
  log.plain("\u{1F504} Genex Tools workspace rules refreshed (AGENTS.md managed block)");
1168
1149
  }
1169
1150
  } else {
@@ -1612,15 +1593,28 @@ function warnPreexisting(log, entries) {
1612
1593
  async function runInit(opts) {
1613
1594
  const log = createLogger({ quiet: opts.quiet });
1614
1595
  const ws = await readWorkspace();
1596
+ const project = ws?.mode === "tools" ? await readProject() : null;
1597
+ if (ws?.mode === "tools" && project) {
1598
+ log.success(
1599
+ `This folder is already connected to the Genex game ${c.cyan(project.slug)} \u2014 nothing to convert.`
1600
+ );
1601
+ log.plain(
1602
+ [
1603
+ ` ${c.cyan("\u2192")} ${c.cyan("npx genex preview")} builds the game and puts it on its draft page.`,
1604
+ ` ${c.cyan("\u2192")} ${c.cyan("npx genex doctor")} checks sign-in, credits and the lanes.`
1605
+ ].join("\n")
1606
+ );
1607
+ return;
1608
+ }
1615
1609
  if (ws?.mode === "tools" && !opts.convert) {
1616
1610
  log.error(
1617
- "This folder is a Genex Tools workspace \u2014 turning it into a hosted Genex game is the user's call."
1611
+ "This folder is a Genex Tools workspace \u2014 putting its game on Genex is the user's call."
1618
1612
  );
1619
1613
  log.plain(
1620
1614
  [
1621
1615
  " Converting means: a Genex project is created for this folder, the source is pushed to a",
1622
1616
  " private Genex-managed repo, and the game gets its own play URL and draft page. The code",
1623
- " stays here and stays theirs; nothing is moved or rewritten.",
1617
+ " stays here and stays theirs, this toolkit stays as it is; nothing is moved or rewritten.",
1624
1618
  ` ${c.cyan("\u2192")} Ask the user first. If they say yes: ${c.cyan("npx genex init --convert")}`,
1625
1619
  ` ${c.cyan("\u2192")} Just want the tools working? ${c.cyan("npx genex doctor")} says what needs fixing.`
1626
1620
  ].join("\n")
@@ -1646,10 +1640,11 @@ async function runInit(opts) {
1646
1640
  for (const t of targets) {
1647
1641
  const src = t.full ? templatesDir : path7.join(templatesDir, "skills");
1648
1642
  const dest = t.full ? t.baseDir : path7.join(t.baseDir, "skills");
1643
+ const family = converting ? skillFamilyFilter("tools", { hosted: true }) : skillFamilyFilter("game");
1649
1644
  const { copied, updated } = await copyTemplates(src, dest, {
1650
1645
  force: opts.force,
1651
1646
  exclude: ["controllers", "motion", "asset-viewer", "blender-service"],
1652
- filter: t.full ? skillFamilyFilter("game") : (rel) => skillFamilyFilter("game")(`skills/${rel}`)
1647
+ filter: t.full ? family : (rel) => family(`skills/${rel}`)
1653
1648
  });
1654
1649
  await pruneRemovedSkills(path7.join(t.baseDir, "skills"), log);
1655
1650
  await writeSkillsMarker(path7.join(t.baseDir, "skills"));
@@ -1693,7 +1688,7 @@ async function runInit(opts) {
1693
1688
  savedToken = false;
1694
1689
  }
1695
1690
  await writeGitignore(process.cwd(), log);
1696
- if (await writeAgentsContract(process.cwd())) {
1691
+ if (!converting && await writeAgentsContract(process.cwd())) {
1697
1692
  log.dim(" wrote the Genex build contract into AGENTS.md (managed block) + CLAUDE.md stub");
1698
1693
  }
1699
1694
  await healAncestorContractsAndReport(log, process.cwd());
@@ -1751,9 +1746,9 @@ async function runInit(opts) {
1751
1746
  log.dim(` saved ${c.cyan(metaPath)}`);
1752
1747
  await writeGameConfigFiles(meta, log);
1753
1748
  if (converting) {
1754
- await fs7.rm(getWorkspacePath(process.cwd()), { force: true }).catch(() => {
1755
- });
1756
- await pruneToolCards(log, { dir: opts.dir, agents: opts.agents });
1749
+ if (await writeToolsContract(process.cwd(), { hosted: true })) {
1750
+ log.dim(" wrote the Genex Tools publishing rules into AGENTS.md (managed block)");
1751
+ }
1757
1752
  await ensureEnvVar(path7.join(process.cwd(), ".env"), "VITE_GENEX_SLUG", meta.slug, log);
1758
1753
  await reportCliEvent(apiUrl, token, "tools_converted");
1759
1754
  }
@@ -1762,17 +1757,19 @@ async function runInit(opts) {
1762
1757
  log.success("All set. \u{1F680}");
1763
1758
  if (converting) {
1764
1759
  log.plain("");
1765
- log.plain(c.bold("Converted: this folder is now a hosted Genex game workspace."));
1760
+ log.plain(c.bold("Connected: this game now has a home on Genex. Publish it as it is \u2014 nothing else changes."));
1766
1761
  log.plain(
1767
1762
  [
1768
- " The Genex Tools cards were replaced by the full game skill set; the AGENTS.md rules",
1769
- " changed with them.",
1763
+ " This is still a Genex Tools workspace (same cards, same rules, assets in ./assets); it gained",
1764
+ ` the publishing commands, the ${c.cyan("genex-tool-publish")} card and three rules about shipping.`,
1765
+ " Do NOT start a design document, a build plan or a rework \u2014 the user asked for it online.",
1770
1766
  " Next, to get the game on its draft page:",
1771
1767
  ` 1. Make sure the game builds to static files with ${c.cyan("npm run build")} into ${c.cyan("dist/")} (or an`,
1772
1768
  ` index.html at the root) and asset paths are relative (Vite: ${c.cyan("base: './'")}).`,
1773
- ` 2. Load the ${c.cyan("genex-threejs-embed-auth")} skill and add the one ${c.cyan("initEmbed()")} call at the`,
1769
+ ` 2. Load the ${c.cyan("genex-threejs-embed-auth")} card and add the one ${c.cyan("initEmbed()")} call at the`,
1774
1770
  " top of the game's boot code.",
1775
- ` 3. ${c.cyan("npx genex preview")} \u2014 then send the user their draft page link.`
1771
+ ` 3. ${c.cyan("npx genex preview")} \u2014 then send the user their draft page link. Its preflight lines`,
1772
+ " are a report to relay in one sentence each, not work to do before the link."
1776
1773
  ].join("\n")
1777
1774
  );
1778
1775
  }
@@ -4832,7 +4829,7 @@ async function deployGame(ctx, opts, log) {
4832
4829
  }
4833
4830
  printMobilePreflight(files, log);
4834
4831
  await printUiAuditPreflight(log);
4835
- await printPipelineStatePreflight(log);
4832
+ if (await workspaceMode(cwd) !== "tools") await printPipelineStatePreflight(log);
4836
4833
  const commit = contentCommit(files);
4837
4834
  log.step("Requesting an upload token\u2026");
4838
4835
  const grant = await getUploadToken(ctx, commit, log);
@@ -8487,7 +8484,8 @@ async function runWaitAll(opts) {
8487
8484
  rows.push(await toRow(e, v, cwd));
8488
8485
  }
8489
8486
  const delivered = [];
8490
- if (!opts.noDownload && await workspaceMode(cwd) === "tools") {
8487
+ const mode = await workspaceMode(cwd);
8488
+ if (!opts.noDownload && mode === "tools") {
8491
8489
  for (const e of ledger) {
8492
8490
  const v = views.get(e.id);
8493
8491
  if (v?.status !== "completed" || !v.files?.length) continue;
@@ -8558,7 +8556,7 @@ async function runWaitAll(opts) {
8558
8556
  }
8559
8557
  log.plain("");
8560
8558
  log.dim(
8561
- " \u{1F4DC} Resumed or compacted? Re-read AGENTS.md + DESIGN.md (continue from its Now: line) before building on."
8559
+ mode === "tools" ? " \u{1F4DC} Resumed or compacted? Re-read AGENTS.md before building on." : " \u{1F4DC} Resumed or compacted? Re-read AGENTS.md + DESIGN.md (continue from its Now: line) before building on."
8562
8560
  );
8563
8561
  }
8564
8562
  async function toRow(e, v, cwd) {
@@ -18614,7 +18612,8 @@ async function runCharacterAdopt(opts) {
18614
18612
  fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character");
18615
18613
  return;
18616
18614
  }
18617
- if (await workspaceMode() === "tools") {
18615
+ const ws = await resolveWorkspace();
18616
+ if (ws.mode === "tools" && !ws.hosted) {
18618
18617
  reportPlatformRefused(log, "character adopt");
18619
18618
  process.exitCode = 1;
18620
18619
  return;
@@ -21105,7 +21104,7 @@ async function runBlender(opts) {
21105
21104
  return serveLocalBlender({ port, log });
21106
21105
  }
21107
21106
  if (sub === "mcp") {
21108
- const { runBlenderMcp } = await import("./blender-mcp-PWOUSXK4.js");
21107
+ const { runBlenderMcp } = await import("./blender-mcp-TTLZ24PD.js");
21109
21108
  return runBlenderMcp();
21110
21109
  }
21111
21110
  if (sub === "seat") {
@@ -22061,6 +22060,18 @@ async function latestPublished() {
22061
22060
  async function runTools(opts) {
22062
22061
  const log = createLogger({ quiet: opts.quiet });
22063
22062
  const project = await readProject();
22063
+ if (project && (await readWorkspace())?.mode === "tools") {
22064
+ log.success(
22065
+ `This folder is already a Genex Tools workspace, connected to the game ${c.cyan(project.slug)}.`
22066
+ );
22067
+ log.plain(
22068
+ [
22069
+ " The tool cards refresh on every command; nothing to set up.",
22070
+ ` ${c.cyan("\u2192")} ${c.cyan("npx genex doctor")} checks sign-in, credits and the lanes.`
22071
+ ].join("\n")
22072
+ );
22073
+ return;
22074
+ }
22064
22075
  if (project) {
22065
22076
  log.error("This folder is already a Genex game project.");
22066
22077
  log.plain(
@@ -22473,9 +22484,10 @@ ${c.bold("Options for `init`")}
22473
22484
  --auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
22474
22485
  --api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
22475
22486
  --no-auth Only scaffold templates; skip authorization.
22476
- --convert In a Genex Tools workspace: with the user's explicit yes, turn
22477
- the folder into a hosted game workspace in place (init is
22478
- otherwise refused there). Ask the user before passing this.
22487
+ --convert In a Genex Tools workspace: with the user's explicit yes, connect
22488
+ the folder to a hosted Genex game in place \u2014 the toolkit stays,
22489
+ the publishing commands open (init is otherwise refused there).
22490
+ Ask the user before passing this.
22479
22491
  --force Overwrite existing files (default: never overwrite).
22480
22492
  --timeout <seconds> How long to wait inline for sign-in approval before handing
22481
22493
  off to 'genex auth' (default: 100). The code itself stays
@@ -23456,14 +23468,14 @@ async function main() {
23456
23468
  setWorkspaceHeader(workspaceHeaderLabel(workspace));
23457
23469
  if (parsed.help || parsed.command === void 0) {
23458
23470
  const perCommand = parsed.command === void 0 ? void 0 : COMMAND_HELP[parsed.command];
23459
- log.plain(perCommand ?? (mode === "tools" ? HELP_TOOLS : HELP));
23471
+ log.plain(perCommand ?? (mode === "tools" && !workspace.hosted ? HELP_TOOLS : HELP));
23460
23472
  return;
23461
23473
  }
23462
23474
  const isBlenderMcp = parsed.command === "blender" && parsed.options.name === "mcp";
23463
23475
  const quietUpdates = Boolean(parsed.options.quiet || parsed.options.json || isBlenderMcp);
23464
23476
  const updateLog = createLogger({ quiet: quietUpdates });
23465
23477
  if (parsed.command !== "init" && parsed.command !== "tools") {
23466
- await syncSkills(updateLog, mode);
23478
+ await syncSkills(updateLog, workspace);
23467
23479
  }
23468
23480
  const updateCheck = startUpdateCheck();
23469
23481
  Sentry2.setTag("command", parsed.command);
@@ -23478,7 +23490,7 @@ async function main() {
23478
23490
  ci: !!process.env.CI
23479
23491
  });
23480
23492
  try {
23481
- if (mode === "tools" && refusedInTools(parsed.command)) {
23493
+ if (platformRefusedIn(workspace, parsed.command)) {
23482
23494
  reportPlatformRefused(log, parsed.command);
23483
23495
  process.exitCode = 1;
23484
23496
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.25.1-dev.618",
3
+ "version": "1.26.0-dev.620",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: genex-tool-publish
3
+ description: Put the game in this folder on the web with Genex — the draft page, `npx genex preview`, then `promote` / `publish`; the link you hand the user, the size limits, and what to do with the preflight lines. Installed once the folder is connected to a hosted Genex game (`npx genex init --convert`). Load it before the first preview.
4
+ ---
5
+
6
+ # Genex Tools · Publish
7
+
8
+ The user built this game themselves and asked for it online. Your job is to
9
+ put it there **as it is** — the code, the structure and the assets stay
10
+ exactly what they are. Publishing adds glue and nothing else; it never starts
11
+ a rework, a design document or a build plan.
12
+
13
+ ## The glue (all of it)
14
+
15
+ 1. **A static build.** The game must build to files with its own build
16
+ command — `npm run build` into `dist/`, or an `index.html` at the root
17
+ with no build step. Asset paths must work from the game's own hosted URL,
18
+ so prefer relative ones (Vite: `base: './'`). Files the game loads at
19
+ runtime — everything in `./assets` — must end up inside the build (Vite:
20
+ a `public/` folder, or copy `assets/` into `dist/` in the build).
21
+ 2. **Player identity.** Load `$genex-threejs-embed-auth` and add the one
22
+ `initEmbed(...)` call at the very top of the boot code. Create the
23
+ renderer and draw a frame BEFORE any `await waitForPlayer()` — on a hosted
24
+ page identity takes seconds, and a game that awaits it first ships black.
25
+ 3. **The CLI as a dev dependency** so plain `npx genex` keeps resolving here
26
+ (setup already did this; check `package.json` if in doubt).
27
+
28
+ Commit the glue as its own small commit when the folder is a git repo, so the
29
+ line between the user's game and what publishing added stays visible.
30
+
31
+ ## Preview
32
+
33
+ ```bash
34
+ npx genex preview
35
+ ```
36
+
37
+ Builds, uploads and puts the build on the game's **draft page** — unlisted,
38
+ only the user can open it. When it reports live, open the hosted build once
39
+ yourself and confirm it loads with no missing files (the dev server forgives
40
+ paths the hosted build will not), then hand over the link.
41
+
42
+ **The link is always the game's page on Genex, never the raw play address,
43
+ never localhost, never a file path:**
44
+
45
+ - draft: `<dashboard>/draft/<slug>`
46
+ - public, once published: `<dashboard>/world/<slug>`
47
+
48
+ `<dashboard>` is the first `dashboardOrigins` entry in `.genex/project.json`
49
+ and `<slug>` is the `slug` there. `preview` prints the exact page link as
50
+ "your game's page" — use that line verbatim.
51
+
52
+ ## The preflight is a report, not a to-do list
53
+
54
+ `preview` prints warning lines before it uploads — an estimated phone GPU
55
+ budget, a music track with no volume slider, a paid asset nobody wired, a
56
+ viewport meta line. None of them blocks the deploy, and none of them was
57
+ asked for. So: **ship first**, hand over the link, then relay each line to
58
+ the user in one plain sentence with an offer ("phones will struggle with the
59
+ textures — want me to shrink them?"). Fix one only on the user's yes. Never
60
+ turn the report into a milestone before the link.
61
+
62
+ ## Draft and public version
63
+
64
+ `preview` updates the **draft** and never touches what players are on. The
65
+ first release lists the game:
66
+
67
+ ```bash
68
+ npx genex publish --categories games
69
+ ```
70
+
71
+ Pick 1–3 categories that fit (`games`, `assets`, `physics`, `terrain`,
72
+ `lighting`, `vfx`); `games` when unsure. Ask before the first publish — "This
73
+ is a private draft only you can open. Say the word and I'll publish it for
74
+ anyone to play." — and celebrate the public page link when it lands.
75
+
76
+ After that first release the game is two versions, and the user only ever
77
+ hears these two words for them: the **draft** (updated by `preview`) and the
78
+ **public version** (what everyone plays). "Publish it", "update it" and "yes"
79
+ after the first release ALL mean:
80
+
81
+ ```bash
82
+ npx genex promote # the exact draft build goes public — no rebuild
83
+ ```
84
+
85
+ Never run `publish` a second time (it would ship an untried rebuild). If
86
+ anything changed since the last `preview`, preview again before promoting.
87
+ Ask once per round of work, in one line, and keep working while you wait.
88
+
89
+ ## Limits and refusals
90
+
91
+ - **Upload size**: ~95 MB per file, ~500 MB in total. Over that, tell the user
92
+ plainly which part is too large (usually a video, model or audio file) and by
93
+ roughly how much; ask before compressing or removing anything.
94
+ - **Terms refusal**: the CLI prints a link, waits for the user's click and
95
+ re-sends by itself — hand the link over and let it finish.
96
+ - **`preview` says the folder is behind another machine** (`stale_source`):
97
+ `npx genex pull` takes the newer draft, then redo the change — never
98
+ `--force` over somebody else's work.
99
+ - **Out of credits or a lane down**: `npx genex doctor` says which; publishing
100
+ itself costs nothing.
@@ -92,6 +92,7 @@ tiling floor and three paid assets nobody loaded.
92
92
  Hosting, publishing, multiplayer, remixing and custom domains are the Genex
93
93
  platform, not this toolkit — those commands are refused in this folder by
94
94
  design, and the refusal says where they live. This workspace generates assets
95
- for a game you build and ship yourself and when the user wants that game
96
- live on Genex with its own URL, the workspace can convert to a hosted game
97
- (the AGENTS.md rules say how).
95
+ for a game you build and ship yourself. When the user wants that game live on
96
+ Genex with its own URL, the AGENTS.md rules say how to offer it; on a yes the
97
+ folder is connected to a hosted game in place — same cards, same rules and
98
+ the `$genex-tool-publish` card arrives with the publishing commands.