@onceworlds/cli 0.12.0 → 0.13.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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ players) with no server code.
7
7
  ## Deploy from GitHub Actions
8
8
 
9
9
  Deploys run from GitHub Actions: GitHub vouches for the repo, so there are no API
10
- keys. The easiest setup is to import the repo at https://onceworlds.com/create?new=github,
10
+ keys. The easiest setup is to import the repo at https://onceworlds.com/upload?new=github,
11
11
  which adds `onceworlds.json` and `.github/workflows/onceworlds.yml` for you (that
12
12
  workflow also builds with npm, pnpm, yarn or bun when there's a build script). To write
13
13
  the same files into a folder before it is on GitHub, run `npx @onceworlds/cli init` there.
@@ -96,7 +96,7 @@ from the running game and commit them under `store/`. `onceworlds doctor` says w
96
96
  whose idea, look and screens are never yours; it adds its own notes as `KIT.md` and its fingerprints as `.onceworlds/kit.json`,
97
97
  so `check` can note what is still the kit's. `--template party` or `starter` starts from a complete game on the SDK, and
98
98
  `--template <name>` from a scene project of Onceworlds Build.
99
- - `onceworlds check [dir]` is what to run before a deploy, for any game (canvas, three.js, PixiJS, Phaser, the DOM, the engine):
99
+ - `onceworlds check [dir]` is what to run before a deploy, for any game (canvas, three.js, PixiJS, Phaser, the DOM, the engine, a Godot or Ren'Py web export):
100
100
  doctor's findings, plus the hosts the platform's CSP won't let the game reach, WebSockets of its own, text too small for a phone,
101
101
  a second countdown under the platform's lobby, shared state written from `Math.random()` or the device's clock, and a `DESIGN.md`
102
102
  silent on originality and the first minute. Then it plays the game on the local platform (`serve`'s) in headless Chrome: it loads
@@ -123,7 +123,7 @@ from the running game and commit them under `store/`. `onceworlds doctor` says w
123
123
  imports it (next to a `AGENTS.md` of your own it writes `ONCEWORLDS.md` and adds a pointer to yours). It never
124
124
  overwrites a file you have: `--force` replaces only what init writes, never your own `AGENTS.md`, `CLAUDE.md` or `.gitignore`.
125
125
  `onceworlds new` writes the same workflow and `.gitignore` into every game it makes.
126
- Then commit, push, and import the repo at https://onceworlds.com/create?new=github.
126
+ Then commit, push, and import the repo at https://onceworlds.com/upload?new=github.
127
127
  - `onceworlds deploy [dir]` uploads only the files that changed and publishes the game
128
128
  (the workflow runs it). It lists every file it skipped because Onceworlds can't serve it. A game's tests never go up: `*.test.js`, `vitest.config.*` and the `test/` folder at the top of the game.
129
129
  - `onceworlds serve [dir]` plays any game in your own browser on this computer, offline and with no login, on a local platform
package/dist/cli.js CHANGED
@@ -717,6 +717,25 @@ var NEW_GAME_RESERVED_SLUGS = /* @__PURE__ */ new Set([
717
717
  ]);
718
718
  var NEW_GAME_RESERVED_PREFIXES = ["onceworlds", "admin", "free-"];
719
719
  var SLUG_AUTH_WORDS = /* @__PURE__ */ new Set(["login", "logon", "signin", "signon", "signup", "verify", "verification", "password", "passwords", "billing"]);
720
+ var NAME_AUTH_WORDS = /* @__PURE__ */ new Set([
721
+ "login",
722
+ "logon",
723
+ "signin",
724
+ "signup",
725
+ "verify",
726
+ "verification",
727
+ "password",
728
+ "billing",
729
+ "account",
730
+ "support",
731
+ "helpdesk",
732
+ "official",
733
+ "staff",
734
+ "admin",
735
+ "administrator",
736
+ "moderator",
737
+ "security"
738
+ ]);
720
739
  var BRANDS = ["onceworlds", "roblox", "robux", "vbucks", "fortnite", "minecraft", "xbox", "playstation", "nintendo"];
721
740
  var LOOKALIKES = {
722
741
  "0": "o",
@@ -795,6 +814,10 @@ function hasBrand(text2) {
795
814
  }
796
815
  return false;
797
816
  }
817
+ function isReservedGameName(name) {
818
+ if (hasBrand(name)) return true;
819
+ return foldedWords(name).some((words3) => NAME_AUTH_WORDS.has(words3.join("")));
820
+ }
798
821
  var DEFAULT_PRODUCTION_BRANCH = "main";
799
822
  function isValidBranchName(name) {
800
823
  return name.length > 0 && name.length <= 200 && /^[A-Za-z0-9._/-]+$/.test(name) && !/^[-/.]|[/.]$|\.\.|\/\/|\/\.|\.lock$|^HEAD$/.test(name);
@@ -1482,9 +1505,26 @@ function checkGame(files) {
1482
1505
  line: at,
1483
1506
  level: "note",
1484
1507
  id: "matches",
1485
- message: `This joins a room but doesn't use the platform's lobby or match lifecycle. That's right for a world people drop in and out of; a game with rounds gets ready-up, a countdown, spectators and pausing from rooms.join({ lobby: 'card' }) (see AGENTS.md, "Matches").`
1508
+ message: `This joins a room but doesn't use the platform's lobby or match lifecycle. That's right for a world people drop in and out of; a game with rounds gets ready-up, a countdown, spectators and pausing from rooms.join({ lobby: 'bar' }), or 'card' (see AGENTS.md, "Matches").`
1486
1509
  });
1487
1510
  }
1511
+ const lobbyLine = (id, level, test, message) => {
1512
+ const file = code.find((f) => test.test(f.text));
1513
+ if (file) found.push({ file: file.path, line: lineOf(file.text, file.text.search(test)), level, id, message });
1514
+ };
1515
+ lobbyLine("hide-lobby", "warn", /\.\s*hideLobby\s*\(/, "room.hideLobby does nothing now: the platform's strip or card always shows in the lobby. Take it out.");
1516
+ lobbyLine("own-countdown", "warn", /\bownCountdown\s*:\s*true\b/, "The 3-2-1 is the platform's. Drop ownCountdown and your own numeral, and start the match's action on matchstart, so one countdown shows.");
1517
+ const gameDrawn = /\blobby\s*:\s*['"]game['"]/;
1518
+ if (gameDrawn.test(all)) {
1519
+ if (/\.\s*setReady\s*\(/.test(all)) lobbyLine("game-lobby", "warn", gameDrawn, "lobby: 'game' with setReady: this game draws Ready itself, so it can't match the rest of the platform (Enter, the same words, a strip that's always there). Use lobby: 'bar' or 'card'.");
1520
+ } else if (/\blobby\s*:\s*['"]bar['"]/.test(all)) {
1521
+ const drawn = /(?:textContent|innerText|innerHTML|fillText\s*\(|label\s*:)\s*=?\s*['"`](?:<[^>]*>)*\s*(Ready|Ready up|Start|Start match|Not ready)\s*(?:<[^>]*>)*['"`]/;
1522
+ const htmlButton = /<button\b[^>]*>\s*(Ready|Ready up|Start match)\s*<\/button>/i;
1523
+ if (/\.\s*(setReady|startMatch)\s*\(/.test(all)) {
1524
+ const test = drawn.test(all) ? drawn : htmlButton;
1525
+ lobbyLine("double-ready", "warn", test, "This joins with lobby: 'bar', and draws a Ready or Start button of its own too: players see two. The strip's button and Enter are the way; take yours out.");
1526
+ }
1527
+ }
1488
1528
  if (/\bDate\s*\.\s*now\s*\(/.test(all) && !/\bonceworlds\s*\.\s*now\s*\(|\bow\s*\.\s*now\s*\(|\bmatchNow\s*\(/.test(all)) {
1489
1529
  found.push({
1490
1530
  file: host.path,
@@ -3187,7 +3227,7 @@ ${line3}
3187
3227
  Next:
3188
3228
  1. Check onceworlds.json: "slug" is your game's link (onceworlds.com/play/<slug>) and "name" is its title.
3189
3229
  2. Commit these files and push the repo to GitHub.
3190
- 3. Open ${options.api}/create?new=github, import the repo, and every push publishes your game.`);
3230
+ 3. Open ${options.api}/upload?new=github, import the repo, and every push publishes your game.`);
3191
3231
  }
3192
3232
  async function readPackage2(dir) {
3193
3233
  try {
@@ -4321,7 +4361,7 @@ ${game.description ? `${game.description}
4321
4361
  - \`npm test\`: the tests of rules.js (\`node --test\`, nothing to install).
4322
4362
  - \`npx @onceworlds/cli check\`: common mistakes, before every deploy. \`shot --story\` shows its first minute.
4323
4363
  - \`npx @onceworlds/cli deploy\` (after \`login\`) pushes a draft to play and publish at ${game.api}/create/${game.slug}. Or push the repo to GitHub
4324
- and import it at ${game.api}/create?new=github: \`.github/workflows/onceworlds.yml\` deploys every push.
4364
+ and import it at ${game.api}/upload?new=github: \`.github/workflows/onceworlds.yml\` deploys every push.
4325
4365
  `
4326
4366
  );
4327
4367
  return files;
@@ -7494,7 +7534,7 @@ var LocalRoom = class {
7494
7534
  const blocker = startBlocker(lobby);
7495
7535
  if (blocker) return this.error(ws, blocker.code, blocker.message);
7496
7536
  const countdown = clampCountdown(msg.countdown, config.countdown ?? LIMITS.matchCountdownMs);
7497
- this.mrec = this.enterStarting(this.mrec, rosterOf(lobby), Date.now(), config, countdown, msg.open === true);
7537
+ this.mrec = this.enterStarting(this.mrec, rosterOf(lobby), Date.now(), config, countdown, true);
7498
7538
  this.stepMatch();
7499
7539
  }
7500
7540
  hostOpen(ws, me, msg, config) {
@@ -8702,7 +8742,7 @@ async function gameSources(root) {
8702
8742
  return out;
8703
8743
  }
8704
8744
  async function usesPlatformMatch(root) {
8705
- return (await gameSources(root)).some((f) => /\b(setReady|startMatch|matchNow)\s*\(|\bmatch\s*\.\s*phase\b|\blobby\s*:\s*['"](card|bar)['"]|\bFlow\s*\.\s*(rounds|turns|coop)\s*\(|\bcreateMatch\s*\(/.test(f.text));
8745
+ return (await gameSources(root)).some((f) => /\b(setReady|startMatch|matchNow)\s*\(|\bmatch\s*\.\s*phase\b|\blobby\s*:\s*['"](card|bar)['"]|\bminPlayers\s*:|\bFlow\s*\.\s*(rounds|turns|coop)\s*\(|\bcreateMatch\s*\(/.test(f.text));
8706
8746
  }
8707
8747
  async function readProbe(p) {
8708
8748
  try {
@@ -12201,9 +12241,10 @@ function lintAny(input) {
12201
12241
  for (const { file } of scripts) {
12202
12242
  const join18 = /\brooms\s*\.\s*join\s*\(\s*\{[^}]*\blobby\s*:\s*["'](card|bar)["'][^}]*\}/.exec(file.text);
12203
12243
  if (!join18 || /\bownCountdown\s*:\s*true\b/.test(join18[0])) continue;
12204
- const all = scripts.map((s) => s.code).join("\n");
12205
- const own = /\bstartsAt\b/.exec(all) && /countdown|count_down|\b3\s*,\s*2\s*,\s*1\b/i.test(all);
12206
- if (own) push(file, join18.index, "warn", "countdown", `This joins with the platform's lobby ('${join18[1]}'), which draws the 3-2-1 itself, and the game seems to draw one too (it reads match.startsAt): players see two countdowns. Say ownCountdown: true in rooms.join to draw yours alone, or drop yours.`);
12244
+ const own = scripts.some(
12245
+ ({ code: text2 }) => [...text2.matchAll(/\bstartsAt\b/g)].some((m) => /fillText|textContent|innerText|innerHTML|\btext\s*\(|draw\w*\s*\(/.test(text2.slice(m.index, m.index + 240).split(/\n\s*\}/)[0]))
12246
+ );
12247
+ if (own) push(file, join18.index, "warn", "countdown", `This joins with the platform's lobby ('${join18[1]}'), which draws the 3-2-1 itself, and the game seems to draw one too (it reads match.startsAt): players see two countdowns. Drop yours.`);
12207
12248
  }
12208
12249
  for (const { file, code: text2 } of scripts) {
12209
12250
  for (const m of text2.matchAll(/\b(setState|send)\s*\(/g)) {
@@ -12298,12 +12339,15 @@ function checkProjectConfig(config) {
12298
12339
  return out;
12299
12340
  }
12300
12341
  const c = config;
12301
- if (typeof c.slug !== "string" || !isValidSlug(c.slug)) {
12342
+ if (typeof c.slug === "string" && isReservedSlug(c.slug, false)) {
12343
+ error("slug", `The link "${c.slug}" in onceworlds.json isn't available.`, `Pick another link, like "star-party": it is the game's address, onceworlds.com/play/<slug>.`);
12344
+ } else if (typeof c.slug !== "string" || !isValidSlug(c.slug)) {
12302
12345
  error("slug", `"slug" in onceworlds.json must be 1-40 lowercase letters, digits or single dashes (got ${JSON.stringify(c.slug)}).`, `Pick a link like "star-party": it is the game's address, onceworlds.com/play/<slug>.`);
12303
12346
  }
12304
12347
  const name = typeof c.name === "string" ? cleanLine(c.name) : "";
12305
12348
  if (!name) error("name", '"name" is required in onceworlds.json.', 'Give the game its title: "name": "Star Party".');
12306
12349
  else if (name.length > LIMITS.gameNameLength) error("name", `"name" in onceworlds.json can be at most ${LIMITS.gameNameLength} characters (it has ${name.length}).`, "Shorten the title.");
12350
+ else if (isReservedGameName(name)) note("name", `A game can't take the name "${name}".`, "Give it a title of its own.");
12307
12351
  if (c.description !== void 0 && typeof c.description !== "string") {
12308
12352
  error("description", '"description" in onceworlds.json must be text.', "Write one or two sentences about the game.");
12309
12353
  } else if (typeof c.description === "string" && c.description.trim().length > DESCRIPTION_SHOWN) {
@@ -12375,9 +12419,23 @@ var DEPLOY_LARGE_FILE_BYTES = 95 * 1024 * 1024;
12375
12419
  function deployFileLimit(path) {
12376
12420
  return LARGE_FILE_EXTENSIONS.includes(extensionOf(path)) ? DEPLOY_LARGE_FILE_BYTES : LIMITS.deployFileBytes;
12377
12421
  }
12422
+ var NOT_A_WEB_GAME = [
12423
+ { pattern: "(^|/)project\\.godot$", text: "This is a Godot project, not its web export. In Godot: Project > Export > Add > Web, with Thread Support off, and name the file index.html." },
12424
+ { pattern: "\\.(rpy|rpyc|rpa)$", text: "This is a Ren'Py game, not its web build. In the Ren'Py launcher: Web, then Build Web Application." },
12425
+ { pattern: "(^|/)(ProjectSettings|Assets)/|(^|/)UnityPlayer\\.dll$", text: "This is a Unity project, not a web build. Build for WebGL with Compression Format: Disabled." },
12426
+ { pattern: "\\.py$", text: "Python games need a web export, such as pygbag." },
12427
+ { pattern: "\\.(exe|apk|aab|ipa|dmg|msi|deb|rpm|appimage|x86_64)$|\\.app/", text: "This is a program to install. Games here run in the browser: export a web version with an index.html." }
12428
+ ];
12429
+ function notAWebGame(paths) {
12430
+ for (const { pattern: pattern2, text: text2 } of NOT_A_WEB_GAME) {
12431
+ const test = new RegExp(pattern2, "i");
12432
+ if (paths.some((path) => test.test(path))) return text2;
12433
+ }
12434
+ return null;
12435
+ }
12378
12436
 
12379
12437
  // src/index.ts
12380
- var CLI_VERSION = "0.12.0";
12438
+ var CLI_VERSION = "0.13.0";
12381
12439
  var PRODUCTION_API = "https://onceworlds.com";
12382
12440
  var LOCAL_API = "http://localhost:8787";
12383
12441
  var USAGE = `Usage:
@@ -12385,7 +12443,7 @@ var USAGE = `Usage:
12385
12443
  onceworlds logout Forget this computer's login
12386
12444
  onceworlds whoami Who you're logged in as
12387
12445
  onceworlds mcp Run as an MCP server, so Claude Code, Codex and other AI tools can build games
12388
- on your account (they push drafts; only you publish, on onceworlds.com/create)
12446
+ on your account (they push drafts; only you publish, on onceworlds.com/library)
12389
12447
  onceworlds new [dir] Start a game on the web platform and the SDK: index.html, game.js on the stack you pick
12390
12448
  (--stack canvas, three, pixi or phaser, from a CDN; canvas by default), rules.js with tests
12391
12449
  that run in Node, DESIGN.md to write first, the platform guide as AGENTS.md and the deploy
@@ -12397,7 +12455,7 @@ var USAGE = `Usage:
12397
12455
  onceworlds init [project-dir] Set a game folder up for Onceworlds: writes onceworlds.json, the GitHub workflow
12398
12456
  that publishes every push, and AGENTS.md and CLAUDE.md for AI coding tools
12399
12457
  onceworlds deploy [project-dir] Upload the game (to ${PRODUCTION_API}): from GitHub Actions it goes live; after login
12400
- it is a draft you play and publish on onceworlds.com/create
12458
+ it is a draft you play and publish from onceworlds.com/library
12401
12459
  onceworlds dev [project-dir] Deploy to a local platform (${LOCAL_API}), then redeploy on every change
12402
12460
  onceworlds serve [project-dir] Play any game in your own browser on this computer, offline, with no login: a local platform
12403
12461
  (the play page, rooms, lobby and SDK, saves and badges in memory) at http://127.0.0.1:8788/.
@@ -12582,7 +12640,8 @@ async function deploy(projectDir, api2, tokenArg, quiet = false, dev2 = false) {
12582
12640
  for (const path of skipped) console.warn(` ${path}`);
12583
12641
  }
12584
12642
  if (!files.some((f) => f.path === "index.html")) {
12585
- throw new CliError(`No index.html in ${root}. Set "dir" in onceworlds.json to the folder that contains it.`);
12643
+ const hint = notAWebGame([...files.map((f) => f.path), ...skipped]);
12644
+ throw new CliError(`No index.html in ${root}. ${hint ? `${hint} Then set "dir" in onceworlds.json to the folder it makes.` : 'Set "dir" in onceworlds.json to the folder that contains it.'}`);
12586
12645
  }
12587
12646
  const sources = await readSources(files);
12588
12647
  const unnamed = config.engine ? void 0 : sources.find((f) => engineImportAt(f.path, f.text) >= 0);
@@ -12888,8 +12947,8 @@ Next:
12888
12947
  npm test (the tests of rules.js: node --test, nothing to install)
12889
12948
  onceworlds check (common mistakes; before every deploy)
12890
12949
  onceworlds shot --story (its first minute, as a new player sees it)
12891
- onceworlds login, then onceworlds deploy (a draft you play and publish on ${api2}/create)
12892
- To publish from a repo: push it to GitHub and import it at ${api2}/create?new=github (its workflow deploys every push).
12950
+ onceworlds login, then onceworlds deploy (a draft you play and publish from ${api2}/library)
12951
+ To publish from a repo: push it to GitHub and import it at ${api2}/upload?new=github (its workflow deploys every push).
12893
12952
  Read AGENTS.md: the platform guide, for you and your AI tool. (onceworlds is npx @onceworlds/cli.)`);
12894
12953
  return;
12895
12954
  }