@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 +3 -3
- package/dist/cli.js +74 -15
- package/dist/emulator.js +178 -152
- package/dist/sdk.js +3 -3
- package/package.json +1 -1
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/
|
|
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/
|
|
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: '
|
|
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}/
|
|
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}/
|
|
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,
|
|
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
|
|
12205
|
-
|
|
12206
|
-
|
|
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
|
|
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.
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
12892
|
-
To publish from a repo: push it to GitHub and import it at ${api2}/
|
|
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
|
}
|