@onceworlds/cli 0.1.1 → 0.2.1

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 (3) hide show
  1. package/README.md +34 -10
  2. package/dist/cli.js +226 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,12 +6,17 @@ players) with no server code.
6
6
 
7
7
  ## Deploy from GitHub Actions
8
8
 
9
- Add `.github/workflows/deploy.yml` to your game's repo:
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/dashboard/new,
11
+ which adds `onceworlds.json` and `.github/workflows/onceworlds.yml` for you (that
12
+ workflow also builds with npm, pnpm, yarn or bun when there's a build script). By
13
+ hand, the minimum is:
10
14
 
11
15
  ```yaml
12
16
  name: Deploy to Onceworlds
13
17
  on:
14
18
  push:
19
+ branches: ['**']
15
20
  permissions:
16
21
  contents: read
17
22
  id-token: write # proves which repo is deploying; no API keys needed
@@ -26,30 +31,49 @@ jobs:
26
31
  - run: npx --yes @onceworlds/cli@latest deploy
27
32
  ```
28
33
 
29
- Pushes to `main` go live at `onceworlds.com/play/<slug>`; other branches get a
30
- preview URL in the job log. Sign in at https://onceworlds.com with the GitHub
34
+ Pushes to the production branch (`main` unless you change it in the dashboard or
35
+ `onceworlds.json`) go live at `onceworlds.com/play/<slug>`; other branches get a
36
+ preview URL in the job log. Log in to https://onceworlds.com with the GitHub
31
37
  account that owns the repo (or any member of its organization) before the first
32
- deploy.
38
+ deploy. If a deploy fails, the dashboard shows why.
33
39
 
34
40
  New games start private: only the repo's owner (or its organization's members)
35
- can play them. When it's ready to share, open the game in the dashboard and click
36
- "Make public" in the Settings tab.
41
+ can play them. To share one, open it in the dashboard and pick Unlisted (anyone
42
+ with the link) or Public (also listed on Onceworlds).
37
43
 
38
44
  ## Project file
39
45
 
40
46
  `onceworlds.json` at the root of the repo:
41
47
 
42
48
  ```json
43
- { "slug": "star-party", "name": "Star Party", "description": "Grab stars with everyone online.", "dir": "." }
49
+ {
50
+ "slug": "kart-rush",
51
+ "name": "Kart Rush",
52
+ "description": "Race friends around twisty tracks.",
53
+ "dir": ".",
54
+ "genre": "Racing",
55
+ "icon": "store/icon.png",
56
+ "thumbnails": ["store/thumb-1.png", "store/thumb-2.png"],
57
+ "badges": [{ "id": "first-win", "name": "Top Step", "description": "Win a race." }]
58
+ }
44
59
  ```
45
60
 
46
61
  `dir` is the folder to publish (for example `dist` after a build); it must contain
47
- `index.html`. `productionBranch` (default `main`) picks the branch that goes live.
62
+ `index.html`. `productionBranch` (default `main`) picks the branch that goes live;
63
+ the dashboard can change it too, and the file wins again only when this value changes.
64
+
65
+ The rest fills in the game page: `genre`, a square `icon` (at least 128×128),
66
+ 16:9 `thumbnails` (at least 480 wide; the first is the cover) and `badges` (`id`,
67
+ `name`, `description` and an optional square `icon`). Images are PNG, JPEG, WebP
68
+ or GIF up to 5 MB, with paths relative to `onceworlds.json`. Live deploys apply
69
+ these fields only when they change, so edits made in the dashboard last until then.
48
70
 
49
71
  ## Commands
50
72
 
51
- - `onceworlds deploy [dir]` uploads only the files that changed and publishes the game.
52
- - `onceworlds dev [dir]` deploys to a local Onceworlds platform and redeploys on every save.
73
+ - `onceworlds deploy [dir]` uploads only the files that changed and publishes the game
74
+ (the workflow runs it). It lists every file it skipped because Onceworlds can't serve it.
75
+ - `onceworlds dev [dir]` deploys to a local Onceworlds platform (for platform development)
76
+ and redeploys on every save.
53
77
 
54
78
  Games talk to the platform through the injected `window.onceworlds` SDK. The
55
79
  starter template's `AGENTS.md` documents it for AI coding tools.
package/dist/cli.js CHANGED
@@ -13,19 +13,25 @@ var OIDC_AUDIENCE = "onceworlds";
13
13
  var LIMITS = {
14
14
  roomMaxPlayersDefault: 16,
15
15
  roomMaxPlayersCap: 50,
16
- publicServersPerGame: 20,
16
+ teamsMax: 8,
17
17
  messageBytes: 16 * 1024,
18
18
  presenceBytes: 1024,
19
19
  stateKeyLength: 64,
20
20
  stateValueBytes: 16 * 1024,
21
21
  stateKeys: 256,
22
22
  messagesPerSecond: 60,
23
+ /** Bytes one player may send to a room per second, on average (bursts up to 4x). */
24
+ roomBytesPerSecond: 128 * 1024,
23
25
  presenceHz: 20,
24
26
  chatLength: 200,
25
27
  chatIntervalMs: 750,
26
28
  chatHistory: 50,
29
+ chatChannelsPerPlayer: 10,
27
30
  saveValueBytes: 64 * 1024,
28
31
  saveKeysPerGame: 100,
32
+ /** All of one player's saves in one game together (guests get less until they sign in). */
33
+ saveBytesPerGame: 1024 * 1024,
34
+ guestSaveBytesPerGame: 128 * 1024,
29
35
  deployFiles: 2e3,
30
36
  deployFileBytes: 25 * 1024 * 1024,
31
37
  deployTotalBytes: 100 * 1024 * 1024,
@@ -33,14 +39,70 @@ var LIMITS = {
33
39
  envValueBytes: 4 * 1024,
34
40
  proxyRequestBytes: 1024 * 1024,
35
41
  proxyResponseBytes: 10 * 1024 * 1024,
42
+ proxyMaxOutputTokens: 8192,
36
43
  proxyCallsPerDay: 2e3,
37
- errorReportsPerPage: 10
44
+ proxyCallsPerDayMax: 1e5,
45
+ proxyCallsPerPlayerPerDay: 100,
46
+ proxyCallsPerNetworkPerDay: 1e3,
47
+ proxyMediaCallsPerDay: 100,
48
+ proxyMediaCallsPerDayMax: 1e4,
49
+ proxyMediaCallsPerPlayerPerDay: 20,
50
+ proxyMediaCallsPerNetworkPerDay: 25,
51
+ errorReportsPerPage: 10,
52
+ gameNameLength: 60,
53
+ gameDescriptionLength: 1e3,
54
+ galleryItems: 10,
55
+ mediaImageBytes: 5 * 1024 * 1024,
56
+ mediaVideoBytes: 30 * 1024 * 1024,
57
+ badgesPerGame: 100,
58
+ badgeNameLength: 50,
59
+ badgeDescriptionLength: 200,
60
+ leaderboardsPerGame: 25,
61
+ leaderboardTop: 100,
62
+ gamesPerOwner: 50,
63
+ /** New games per creator in any rolling 24-hour period. Redeploys do not count. */
64
+ gamesCreatedPerCreatorPerDay: 3,
65
+ deploysPerOwnerPerDay: 100,
66
+ /** Finished versions retained per game, including its live version. */
67
+ deploymentsKeptPerGame: 10,
68
+ deployBytesPerOwnerPerDay: 2 * 1024 * 1024 * 1024,
69
+ bioLength: 500,
70
+ friendsMax: 200,
71
+ friendRequestsMax: 100,
72
+ passwordMinLength: 8,
73
+ passwordMaxLength: 72,
74
+ /** Account creation attempts, with or without email, per network (IP, or IPv6 /64) in 24 hours. */
75
+ signupsPerNetworkPerDay: 100,
76
+ /** New guest players per network per day (each one can hold saves). */
77
+ guestsPerNetworkPerDay: 300,
78
+ /** Visits a game gets per network (IP, or IPv6 /64) per day: a script can't pump the count. */
79
+ visitsPerNetworkPerDay: 10,
80
+ /** Seconds of visible play before a play counts as a visit. */
81
+ visitSeconds: 10,
82
+ /** Distinct errors kept per deploy of a game (more are counted by nobody). */
83
+ errorsPerDeployment: 100,
84
+ /** On-screen controls: buttons beside the thumbstick, and their labels. */
85
+ controlButtons: 4,
86
+ controlLabelLength: 12,
87
+ /** Friends in one party, the leader included. */
88
+ partySize: 6,
89
+ partyChatHistory: 50,
90
+ /** Messages outside games: saved chats between friends. */
91
+ messageLength: 500,
92
+ messagesPerPage: 50,
93
+ groupChatMembers: 20,
94
+ groupChatNameLength: 40,
95
+ conversationsListed: 50,
96
+ notificationsListed: 50,
97
+ /** Friends one game invite can go to. */
98
+ invitesPerRequest: 10
38
99
  };
39
100
  var CONTENT_TYPES = {
40
101
  html: "text/html; charset=utf-8",
41
102
  htm: "text/html; charset=utf-8",
42
103
  js: "text/javascript; charset=utf-8",
43
104
  mjs: "text/javascript; charset=utf-8",
105
+ cjs: "text/javascript; charset=utf-8",
44
106
  css: "text/css; charset=utf-8",
45
107
  json: "application/json; charset=utf-8",
46
108
  map: "application/json; charset=utf-8",
@@ -66,6 +128,7 @@ var CONTENT_TYPES = {
66
128
  opus: "audio/ogg",
67
129
  wav: "audio/wav",
68
130
  m4a: "audio/mp4",
131
+ aac: "audio/aac",
69
132
  flac: "audio/flac",
70
133
  mp4: "video/mp4",
71
134
  webm: "video/webm",
@@ -76,6 +139,11 @@ var CONTENT_TYPES = {
76
139
  mtl: "text/plain; charset=utf-8",
77
140
  fbx: "application/octet-stream",
78
141
  wasm: "application/wasm",
142
+ // Shaders, loaded as text.
143
+ glsl: "text/plain; charset=utf-8",
144
+ vert: "text/plain; charset=utf-8",
145
+ frag: "text/plain; charset=utf-8",
146
+ wgsl: "text/plain; charset=utf-8",
79
147
  pck: "application/octet-stream",
80
148
  data: "application/octet-stream",
81
149
  woff: "font/woff",
@@ -108,6 +176,82 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set(["www", "api", "play", "admin", "st
108
176
  function isValidSlug(slug) {
109
177
  return SLUG_RE.test(slug) && !slug.includes("--") && !RESERVED_SLUGS.has(slug);
110
178
  }
179
+ var DEFAULT_PRODUCTION_BRANCH = "main";
180
+ var BADGE_KEY_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
181
+ var GENRES = [
182
+ "Action",
183
+ "Adventure",
184
+ "Fighting",
185
+ "Obby",
186
+ "Puzzle",
187
+ "Racing",
188
+ "Roleplay",
189
+ "Shooter",
190
+ "Simulation",
191
+ "Sports",
192
+ "Strategy",
193
+ "Survival",
194
+ "Tycoon",
195
+ "Horror",
196
+ "Party",
197
+ "Other"
198
+ ];
199
+ var MAX_IMAGE_SIDE = 4096;
200
+ function imageInfo(bytes) {
201
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
202
+ const ascii = (at, text) => text.split("").every((c, i) => bytes[at + i] === c.charCodeAt(0));
203
+ if (bytes.length >= 24 && bytes[0] === 137 && ascii(1, "PNG\r\n\n") && ascii(12, "IHDR")) {
204
+ return { type: "image/png", width: view.getUint32(16), height: view.getUint32(20) };
205
+ }
206
+ if (bytes.length >= 10 && (ascii(0, "GIF87a") || ascii(0, "GIF89a"))) {
207
+ return { type: "image/gif", width: view.getUint16(6, true), height: view.getUint16(8, true) };
208
+ }
209
+ if (bytes.length >= 30 && ascii(0, "RIFF") && ascii(8, "WEBP")) {
210
+ if (ascii(12, "VP8 ")) {
211
+ return { type: "image/webp", width: view.getUint16(26, true) & 16383, height: view.getUint16(28, true) & 16383 };
212
+ }
213
+ if (ascii(12, "VP8L") && bytes[20] === 47) {
214
+ const bits = view.getUint32(21, true);
215
+ return { type: "image/webp", width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
216
+ }
217
+ if (ascii(12, "VP8X")) {
218
+ const width = (bytes[24] | bytes[25] << 8 | bytes[26] << 16) + 1;
219
+ const height = (bytes[27] | bytes[28] << 8 | bytes[29] << 16) + 1;
220
+ return { type: "image/webp", width, height };
221
+ }
222
+ return null;
223
+ }
224
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
225
+ let at = 2;
226
+ while (at + 9 < bytes.length) {
227
+ if (bytes[at] !== 255) return null;
228
+ const marker = bytes[at + 1];
229
+ if (marker === 255) {
230
+ at++;
231
+ continue;
232
+ }
233
+ const length = view.getUint16(at + 2);
234
+ const isFrame = marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
235
+ if (isFrame) return { type: "image/jpeg", width: view.getUint16(at + 7), height: view.getUint16(at + 5) };
236
+ at += 2 + length;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+ function imageSizeProblem(slot, width, height) {
242
+ if (width < 1 || height < 1 || width > MAX_IMAGE_SIDE || height > MAX_IMAGE_SIDE) {
243
+ return `Images can be at most ${MAX_IMAGE_SIDE}\xD7${MAX_IMAGE_SIDE} pixels.`;
244
+ }
245
+ if (slot === "thumbnail") {
246
+ if (Math.abs(width * 9 - height * 16) > 0.02 * width * 9) return "Thumbnails must be 16:9, like 1920\xD71080.";
247
+ if (width < 480) return "Thumbnails must be at least 480 pixels wide.";
248
+ return null;
249
+ }
250
+ const min = slot === "icon" ? 128 : 64;
251
+ if (Math.abs(width - height) > Math.max(2, 0.01 * Math.max(width, height))) return "This image must be square.";
252
+ if (width < min) return `This image must be at least ${min}\xD7${min} pixels.`;
253
+ return null;
254
+ }
111
255
 
112
256
  // src/index.ts
113
257
  var PRODUCTION_API = "https://onceworlds.com";
@@ -118,15 +262,18 @@ var USAGE = `Usage:
118
262
 
119
263
  Options:
120
264
  --api <url> Platform URL (default: $ONCEWORLDS_API, else the ones above)
121
- --token <t> Deploy token (default: $ONCEWORLDS_TOKEN, GitHub Actions OIDC, or "dev" on localhost)
265
+ --token <t> Local platform's deploy token (default: "dev")
266
+
267
+ Deploys to onceworlds.com run from GitHub Actions: the repo's Onceworlds
268
+ workflow runs this on every push, and GitHub vouches for the repo.
122
269
 
123
270
  The project directory needs an onceworlds.json:
124
- { "slug": "star-party", "name": "Star Party", "dir": "." }
271
+ { "slug": "my-game", "name": "My Game", "dir": "." }
125
272
  `;
126
273
  var CliError = class extends Error {
127
274
  };
128
275
  var QUIET_SKIPS = /* @__PURE__ */ new Set(["node_modules", "onceworlds.json", "package.json", "package-lock.json", "tsconfig.json", "jsconfig.json", "LICENSE"]);
129
- var QUIET_EXTENSIONS = /* @__PURE__ */ new Set(["md", "ts", "tsx", "jsx", "lock", "log", "yml", "yaml", "sh"]);
276
+ var QUIET_EXTENSIONS = /* @__PURE__ */ new Set(["md", "ts", "tsx", "jsx", "lock", "lockb", "log", "yml", "yaml", "sh"]);
130
277
  async function main() {
131
278
  const { values, positionals } = parseArgs({
132
279
  allowPositionals: true,
@@ -152,12 +299,17 @@ ${USAGE}`);
152
299
  async function deploy(projectDir, api, tokenArg, quiet = false) {
153
300
  const config = await loadConfig(projectDir);
154
301
  const root = resolve(projectDir, config.dir ?? ".");
155
- const files = await collectFiles(root, quiet);
302
+ const { files, skipped } = await collectFiles(root);
303
+ if (skipped.length > 0) {
304
+ console.warn(` ! ${skipped.length} unsupported file${skipped.length === 1 ? "" : "s"} not deployed:`);
305
+ for (const path of skipped) console.warn(` ${path}`);
306
+ }
156
307
  if (!files.some((f) => f.path === "index.html")) {
157
308
  throw new CliError(`No index.html in ${root}. Set "dir" in onceworlds.json to the folder that contains it.`);
158
309
  }
159
310
  const total = files.reduce((sum, f) => sum + f.size, 0);
160
311
  if (total > LIMITS.deployTotalBytes) throw new CliError(`Game is ${formatBytes(total)}; the limit is ${formatBytes(LIMITS.deployTotalBytes)}.`);
312
+ const { store, images } = await loadStore(projectDir, config);
161
313
  if (!quiet) console.log(`Deploying ${config.slug} (${files.length} files, ${formatBytes(total)}) to ${api}`);
162
314
  const token = await getToken(api, tokenArg);
163
315
  const start = await send("POST", `${api}/api/deploy`, token, {
@@ -165,12 +317,13 @@ async function deploy(projectDir, api, tokenArg, quiet = false) {
165
317
  slug: config.slug,
166
318
  name: config.name,
167
319
  description: config.description ?? "",
168
- productionBranch: config.productionBranch ?? "main"
320
+ productionBranch: config.productionBranch ?? DEFAULT_PRODUCTION_BRANCH,
321
+ store
169
322
  },
170
323
  files: files.map(({ path, hash, size }) => ({ path, hash, size })),
171
324
  git: gitInfo(projectDir)
172
325
  });
173
- const byHash = new Map(files.map((f) => [f.hash, f]));
326
+ const byHash = new Map([...images, ...files].map((f) => [f.hash, f]));
174
327
  const uploads = start.missing.map((hash) => byHash.get(hash));
175
328
  if (uploads.length > 0 && !quiet) {
176
329
  console.log(` \u2191 uploading ${uploads.length} changed file(s), ${formatBytes(uploads.reduce((s, f) => s + f.size, 0))}`);
@@ -181,8 +334,9 @@ async function deploy(projectDir, api, tokenArg, quiet = false) {
181
334
  });
182
335
  const result = await send("POST", `${api}/api/deploy/${start.deploymentId}/finalize`, start.uploadToken, {});
183
336
  if (!quiet) {
337
+ const branch = result.productionBranch ?? start.productionBranch;
184
338
  if (result.production) console.log(`\u2713 Live: ${result.playUrl}`);
185
- else console.log(`\u2713 Preview (this branch isn't ${config.productionBranch ?? "main"}): ${result.previewUrl}`);
339
+ else console.log(`\u2713 Preview${branch ? ` (only ${branch} goes live)` : ""}: ${result.previewUrl}`);
186
340
  for (const note of result.notes ?? []) console.log(` ${note}`);
187
341
  }
188
342
  return result;
@@ -195,7 +349,8 @@ async function dev(projectDir, api, tokenArg) {
195
349
  Play (reloads on every change): ${first.playUrl}?watch
196
350
  Watching ${root}. Press Ctrl+C to stop.
197
351
  `);
198
- let lastSignature = signature(await collectFiles(root, true));
352
+ const projectSignature = async () => signature((await collectFiles(root)).files) + await readFile(join(projectDir, "onceworlds.json"), "utf8").catch(() => "");
353
+ let lastSignature = await projectSignature();
199
354
  let timer;
200
355
  let running = false;
201
356
  let again = false;
@@ -206,7 +361,7 @@ async function dev(projectDir, api, tokenArg) {
206
361
  }
207
362
  running = true;
208
363
  try {
209
- const current = signature(await collectFiles(root, true));
364
+ const current = await projectSignature();
210
365
  if (current !== lastSignature) {
211
366
  await deploy(projectDir, api, tokenArg, true);
212
367
  lastSignature = current;
@@ -250,7 +405,64 @@ ${USAGE}` : `Could not parse ${file}: ${err.message}`);
250
405
  if (typeof config.name !== "string" || !config.name.trim()) throw new CliError('"name" is required in onceworlds.json.');
251
406
  return config;
252
407
  }
253
- async function collectFiles(root, quiet) {
408
+ async function loadStore(projectDir, config) {
409
+ const images = /* @__PURE__ */ new Map();
410
+ const image = async (value, slot, field) => {
411
+ if (typeof value !== "string" || !value.trim()) throw new CliError(`"${field}" in onceworlds.json must be the path of an image, like "store/icon.png".`);
412
+ const abs = resolve(projectDir, value);
413
+ if (!abs.startsWith(projectDir + sep)) throw new CliError(`"${field}" in onceworlds.json (${value}) must be inside the project folder.`);
414
+ let data;
415
+ try {
416
+ data = await readFile(abs);
417
+ } catch {
418
+ throw new CliError(`"${field}" in onceworlds.json: ${value} doesn't exist.`);
419
+ }
420
+ if (data.byteLength > LIMITS.mediaImageBytes) {
421
+ throw new CliError(`${value} is ${formatBytes(data.byteLength)}; game page images can be at most ${formatBytes(LIMITS.mediaImageBytes)}.`);
422
+ }
423
+ const info = imageInfo(data);
424
+ if (!info) throw new CliError(`${value} ("${field}") must be a PNG, JPEG, WebP or GIF.`);
425
+ const problem = imageSizeProblem(slot, info.width, info.height);
426
+ if (problem) throw new CliError(`${value} ("${field}") is ${info.width}\xD7${info.height}. ${problem}`);
427
+ const hash = createHash("sha256").update(data).digest("hex");
428
+ images.set(hash, { path: value, abs, hash, size: data.byteLength });
429
+ return { hash, size: data.byteLength };
430
+ };
431
+ const list = (value, max, field) => {
432
+ if (value === void 0) return [];
433
+ if (!Array.isArray(value)) throw new CliError(`"${field}" in onceworlds.json must be a list.`);
434
+ if (value.length > max) throw new CliError(`"${field}" in onceworlds.json can have at most ${max} items.`);
435
+ return value;
436
+ };
437
+ let genre = "";
438
+ if (config.genre !== void 0 && config.genre !== "") {
439
+ genre = GENRES.find((g) => g.toLowerCase() === String(config.genre).toLowerCase()) ?? "";
440
+ if (!genre) throw new CliError(`"genre" in onceworlds.json must be one of: ${GENRES.join(", ")}.`);
441
+ }
442
+ const icon = config.icon === void 0 ? null : await image(config.icon, "icon", "icon");
443
+ const thumbnails = [];
444
+ for (const [i, path] of list(config.thumbnails, LIMITS.galleryItems, "thumbnails").entries()) {
445
+ thumbnails.push(await image(path, "thumbnail", `thumbnails[${i}]`));
446
+ }
447
+ const badges = [];
448
+ for (const [i, value] of list(config.badges, LIMITS.badgesPerGame, "badges").entries()) {
449
+ const b = value ?? {};
450
+ const field = `badges[${i}]`;
451
+ const key = typeof b.id === "string" ? b.id : "";
452
+ if (!BADGE_KEY_RE.test(key)) throw new CliError(`"${field}.id" in onceworlds.json must be lowercase letters, digits and dashes, like "first-win".`);
453
+ if (badges.some((other) => other.key === key)) throw new CliError(`Two badges in onceworlds.json have the id "${key}".`);
454
+ const name = typeof b.name === "string" ? b.name.trim() : "";
455
+ if (!name || name.length > LIMITS.badgeNameLength) throw new CliError(`Badge "${key}" needs a name of 1-${LIMITS.badgeNameLength} characters.`);
456
+ const description = typeof b.description === "string" ? b.description.trim() : "";
457
+ if (description.length > LIMITS.badgeDescriptionLength) {
458
+ throw new CliError(`Badge "${key}": descriptions can be at most ${LIMITS.badgeDescriptionLength} characters.`);
459
+ }
460
+ const badgeIcon = b.icon === void 0 ? null : await image(b.icon, "badge", `${field}.icon`);
461
+ badges.push({ key, name, description, icon: badgeIcon });
462
+ }
463
+ return { store: { genre, icon, thumbnails, badges }, images: [...images.values()] };
464
+ }
465
+ async function collectFiles(root) {
254
466
  const files = [];
255
467
  const skipped = [];
256
468
  async function walk(dir, prefix) {
@@ -280,9 +492,8 @@ async function collectFiles(root, quiet) {
280
492
  }
281
493
  }
282
494
  await walk(root, "");
283
- if (skipped.length > 0 && !quiet) console.warn(` ! skipped unsupported files: ${skipped.join(", ")}`);
284
495
  if (files.length > LIMITS.deployFiles) throw new CliError(`Too many files (${files.length}); the limit is ${LIMITS.deployFiles}.`);
285
- return files;
496
+ return { files, skipped };
286
497
  }
287
498
  function signature(files) {
288
499
  return files.map((f) => `${f.path}:${f.hash}`).join("\n");
@@ -299,7 +510,6 @@ function gitInfo(cwd) {
299
510
  }
300
511
  async function getToken(api, tokenArg) {
301
512
  if (tokenArg) return tokenArg;
302
- if (process.env.ONCEWORLDS_TOKEN) return process.env.ONCEWORLDS_TOKEN;
303
513
  const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
304
514
  const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
305
515
  if (requestUrl && requestToken) {
@@ -312,7 +522,7 @@ async function getToken(api, tokenArg) {
312
522
  const host = new URL(api).hostname;
313
523
  if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host.endsWith(".localhost")) return "dev";
314
524
  throw new CliError(
315
- "No deploy credentials. In GitHub Actions, add `permissions: id-token: write` to the workflow. Elsewhere, set ONCEWORLDS_TOKEN."
525
+ "Deploys to Onceworlds run from GitHub Actions: push to GitHub and the repo's Onceworlds workflow deploys (it needs `permissions: id-token: write`)."
316
526
  );
317
527
  }
318
528
  async function send(method, url, token, body) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onceworlds/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Deploy games to Onceworlds.",
5
5
  "homepage": "https://onceworlds.com",
6
6
  "type": "module",