@xgamesfun/xcore 0.1.0 → 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 (72) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +183 -84
  3. package/bin/xcore.mjs +13 -8
  4. package/commands/add.mjs +11 -8
  5. package/commands/adopt.mjs +9 -7
  6. package/commands/backend.mjs +2 -2
  7. package/commands/check.mjs +4 -4
  8. package/commands/db.mjs +218 -0
  9. package/commands/dev.mjs +8 -6
  10. package/commands/domain.mjs +8 -8
  11. package/commands/embed.mjs +3 -3
  12. package/commands/files.mjs +106 -0
  13. package/commands/login.mjs +9 -17
  14. package/commands/logout.mjs +15 -4
  15. package/commands/loot.mjs +2 -2
  16. package/commands/plan.mjs +22 -19
  17. package/commands/publish.mjs +54 -21
  18. package/commands/secret.mjs +6 -6
  19. package/docs/SERVER.md +218 -18
  20. package/lib/add.mjs +265 -40
  21. package/lib/browser.mjs +11 -2
  22. package/lib/check.mjs +10 -9
  23. package/lib/cli.mjs +24 -9
  24. package/lib/client/gamesfun.js +242 -122
  25. package/lib/client/godot/gamesfun.gd +17 -11
  26. package/lib/client/godot/gamesfun_peer.gd +2 -2
  27. package/lib/client/server/0001_init.sql +10 -0
  28. package/lib/client/server/database.js +271 -109
  29. package/lib/client/server/gf-db.js +55 -0
  30. package/lib/client/server/gf-docs.js +56 -0
  31. package/lib/client/server/rooms.js +16 -9
  32. package/lib/client/server/rpc.js +7 -7
  33. package/lib/client/server/stock.js +37 -0
  34. package/lib/client/server/vouchers.js +127 -46
  35. package/lib/client/unity/GamesFun.cs +15 -12
  36. package/lib/client/unity/GamesFun.jslib +1 -1
  37. package/lib/config-dir.mjs +1 -1
  38. package/lib/credentials.mjs +10 -5
  39. package/lib/db.mjs +355 -0
  40. package/lib/dev/d1.mjs +486 -0
  41. package/lib/dev/files.mjs +247 -0
  42. package/lib/dev/gf-local.js +69 -3
  43. package/lib/dev/index.mjs +68 -20
  44. package/lib/dev/limits.mjs +2 -2
  45. package/lib/dev/platform.mjs +111 -12
  46. package/lib/dev/server.mjs +54 -5
  47. package/lib/dev/tools.mjs +71 -18
  48. package/lib/files.mjs +208 -0
  49. package/lib/open.mjs +1 -1
  50. package/lib/play-policy.mjs +14 -5
  51. package/lib/publish/adopt.mjs +73 -33
  52. package/lib/publish/client.mjs +130 -20
  53. package/lib/publish/gate.mjs +4 -2
  54. package/lib/publish/hosting.mjs +5 -5
  55. package/lib/publish/index.mjs +162 -40
  56. package/lib/publish/oidc.mjs +26 -5
  57. package/lib/publish/page.mjs +1 -1
  58. package/lib/publish/plans.mjs +19 -12
  59. package/lib/publish/scan.mjs +78 -102
  60. package/lib/publish/secrets.mjs +4 -5
  61. package/lib/publish/site.mjs +9 -10
  62. package/lib/publish/tree.mjs +8 -8
  63. package/lib/secret-patterns.mjs +53 -16
  64. package/lib/secret-store.mjs +2 -2
  65. package/lib/server/bundle.mjs +113 -13
  66. package/lib/server/contract.mjs +440 -16
  67. package/lib/server/data-calls.mjs +136 -0
  68. package/lib/server/secrets.mjs +9 -7
  69. package/lib/solana-rpc.mjs +5 -5
  70. package/lib/state-folder.mjs +23 -0
  71. package/package.json +4 -4
  72. package/skills/bring-your-own-game/SKILL.md +10 -9
@@ -0,0 +1,106 @@
1
+ // `xcore files ls|put|get|rm|usage`: the game's project files (env.GF.files), on this machine (the ones `xcore dev`
2
+ // serves) or, with --live, on xgames.fun (../lib/files.mjs).
3
+ import { existsSync } from 'node:fs';
4
+ import { writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
7
+ import { named, resolveGame } from '../lib/games.mjs';
8
+ import { filesGet, filesList, filesPut, filesRemove, filesUsage } from '../lib/files.mjs';
9
+ import { CMD } from '../lib/command.mjs';
10
+
11
+ const usage = `Usage: xcore files ls [prefix/] [--dir <game>] [--live] [--json]
12
+ xcore files put <file> [path] [--public] [--type <mime>] [--dir <game>] [--live] [--json]
13
+ xcore files get <path> [--out <file>] [--force] [--dir <game>] [--live] [--json]
14
+ xcore files rm <path> [--dir <game>] [--live] [--json]
15
+ xcore files usage [--dir <game>] [--live] [--json]
16
+
17
+ The game's project files: files you or the game's server write, never players (replays, generated levels, content
18
+ packs, a large saved world). The server reads and writes them with env.GF.files (docs/SERVER.md, "Project files").
19
+ Without --live, the ones on this machine that ${CMD} dev gives the server (kept in .gamesfun/dev/files/, never
20
+ published); with --live, the game's files on xgames.fun (each call kept in the game's audit log).
21
+ ls [prefix/] the files, or those in one folder ("replays/")
22
+ put <file> [path] a file from this machine, at [path] (default: its name). --public serves it at
23
+ https://<slug>.xgamesusercontent.com/<path>, another site (here: the files origin xcore dev
24
+ prints); without it only the game's server reads it. Up to 25 MB a file
25
+ get <path> a file's bytes, into --out <file> (default: its name here; --force writes over one)
26
+ rm <path> delete one
27
+ usage the bytes kept, against the game's cap (Free: 100 MB)
28
+ A path is letters, digits, . _ and -, with / between folders: levels/forest-3.json.
29
+ --dir <game> names the game's folder (default: the one you are in).`;
30
+
31
+ const mb = n =>
32
+ n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KB` : `${n} bytes`;
33
+
34
+ export default {
35
+ name: 'files',
36
+ summary: "the game's project files (env.GF.files): ls, put, get, rm, usage; here, or --live on xgames.fun",
37
+ usage,
38
+ run: args =>
39
+ guard(async () => {
40
+ const { values, positionals } = parse(
41
+ args,
42
+ {
43
+ live: { type: 'boolean' },
44
+ public: { type: 'boolean' },
45
+ type: { type: 'string' },
46
+ out: { type: 'string' },
47
+ force: { type: 'boolean' },
48
+ dir: { type: 'string' },
49
+ },
50
+ usage,
51
+ );
52
+ const [verb, a, b] = positionals;
53
+ if (!['ls', 'put', 'get', 'rm', 'usage'].includes(verb)) throw new UsageError(usage);
54
+ if (['put', 'get', 'rm'].includes(verb) && !a)
55
+ throw new UsageError(`${verb} needs ${verb === 'put' ? 'a file' : 'a path'}\n\n${usage}`);
56
+ const dir = await resolveGame(values.dir);
57
+ const live = !!values.live;
58
+ const where = live ? 'on xgames.fun' : 'on this machine';
59
+ const json = values.json ? printJson : null;
60
+
61
+ if (verb === 'ls') {
62
+ const r = await filesList(dir, { live, prefix: a ?? '' });
63
+ if (json) return json(r);
64
+ if (!r.items.length) console.log(`${named(dir)} has no project files ${where}${a ? ` in ${a}` : ''}.`);
65
+ for (const f of r.items)
66
+ console.log(`${f.public ? 'public ' : 'private'} ${mb(f.bytes).padStart(9)} ${f.path} (${f.type})`);
67
+ if (r.after) console.log(`… more after ${r.after}`);
68
+ return 0;
69
+ }
70
+ if (verb === 'put') {
71
+ const r = await filesPut(dir, a, b ?? null, { live, isPublic: !!values.public, type: values.type ?? null });
72
+ if (json) return json(r);
73
+ console.log(
74
+ `Put ${r.path} ${where} (${mb(r.bytes)}${values.public ? ', public' : ', only the game’s server reads it'}).${r.url ? `\n ${r.url}` : values.public && !live ? `\n served at the files origin ${CMD} dev prints, /${r.path}` : ''}`,
75
+ );
76
+ return 0;
77
+ }
78
+ if (verb === 'get') {
79
+ const r = await filesGet(dir, a, { live });
80
+ const out = path.resolve(values.out ?? path.basename(a));
81
+ if (json && !values.out) {
82
+ const { body, ...facts } = r;
83
+ return json({ ...facts, base64: body.toString('base64') });
84
+ }
85
+ if (existsSync(out) && !values.force)
86
+ throw new UsageError(`${out} is there already: --force writes over it, or --out <file>\n\n${usage}`);
87
+ await writeFile(out, r.body);
88
+ const { body: _body, ...facts } = r;
89
+ if (json) return json({ ...facts, file: out });
90
+ console.log(`Got ${r.path} from ${where} (${mb(r.bytes)}, ${r.type}) into ${out}.`);
91
+ return 0;
92
+ }
93
+ if (verb === 'rm') {
94
+ const r = await filesRemove(dir, a, { live });
95
+ if (json) return json(r);
96
+ console.log(r.deleted ? `Deleted ${a} ${where}.` : `${a} was not there ${where}.`);
97
+ return 0;
98
+ }
99
+ const r = await filesUsage(dir, { live });
100
+ if (json) return json(r);
101
+ console.log(
102
+ `${named(dir)}'s project files ${where}: ${r.files} file${r.files === 1 ? '' : 's'}, ${mb(r.bytes)} of ${mb(r.cap)}.`,
103
+ );
104
+ return 0;
105
+ }, args),
106
+ };
@@ -1,39 +1,31 @@
1
1
  import { createInterface } from 'node:readline/promises';
2
2
  import { guard, parse, printJson } from '../lib/cli.mjs';
3
3
  import { openUrl } from '../lib/open.mjs';
4
- import { login, savedLogin, studioOrigin } from '../lib/publish/client.mjs';
4
+ import { login, signInState, storeWords, studioOrigin } from '../lib/publish/client.mjs';
5
5
  import { storeName } from '../lib/credentials.mjs';
6
6
  import { CMD } from '../lib/command.mjs';
7
7
 
8
8
  const usage = `Usage: xcore login [--code] [--status] [--json]
9
9
 
10
- Sign in to games.fun so you can publish your games (${CMD} publish). Your browser opens games.fun: sign in
10
+ Sign in to xgames.fun so you can publish your games (${CMD} publish). Your browser opens xgames.fun: sign in
11
11
  there and approve this computer. xcore never sees your password; it keeps a publish token for 30 days in
12
12
  ${storeName()} (never in a game or in this folder). ${CMD} logout ends it.
13
13
  --code no browser on this machine (SSH, a container): open the printed link anywhere and paste the code it shows
14
14
  --status who this computer is signed in as, and where the token is kept
15
- GAMESFUN_STUDIO=<origin> signs in to another games.fun (a local Studio while developing).`;
15
+ GAMESFUN_STUDIO=<origin> signs in to another xgames.fun (a local Studio while developing).`;
16
16
 
17
17
  export default {
18
18
  name: 'login',
19
- summary: 'sign in to games.fun so you can publish your games',
19
+ summary: 'sign in to xgames.fun so you can publish your games',
20
20
  usage,
21
21
  run: args =>
22
22
  guard(async () => {
23
23
  const { values } = parse(args, { code: { type: 'boolean' }, status: { type: 'boolean' } }, usage);
24
24
  const origin = studioOrigin();
25
25
  if (values.status) {
26
- const s = await savedLogin(origin);
27
- const out = s
28
- ? { studio: origin, email: s.email, expiresAt: s.expiresAt, store: s.store }
29
- : { studio: origin, signedIn: false };
30
- if (values.json) printJson(out);
31
- else
32
- console.log(
33
- s
34
- ? `Signed in to ${origin} as ${s.email} until ${s.expiresAt} (token in ${s.store === 'file' ? 'a file only you can read' : s.store}).`
35
- : `Not signed in to ${origin}. Run: ${CMD} login`,
36
- );
26
+ const { token: _token, words, ...s } = await signInState(origin);
27
+ if (values.json) printJson({ ...s, signedIn: s.state === 'signed-in' });
28
+ else console.log(s.state === 'signed-in' ? words : `${words} Run: ${s.next}`);
37
29
  return 0;
38
30
  }
39
31
  const rl = values.code ? createInterface({ input: process.stdin, output: process.stderr }) : null;
@@ -48,11 +40,11 @@ export default {
48
40
  if (values.json) printJson({ studio: origin, ...r });
49
41
  else
50
42
  console.log(
51
- `Signed in to ${origin} as ${r.email}. The token is kept in ${r.store === 'file' ? 'a file only you can read' : r.store === 'dpapi' ? 'a file encrypted for your Windows user' : r.store === 'keychain' ? 'the macOS keychain' : 'the Secret Service'} until ${new Date(r.expiresAt).toISOString().slice(0, 10)}. Next: ${CMD} publish`,
43
+ `Signed in to ${origin} as ${r.email}. The token is kept in ${storeWords(r.store)} until ${new Date(r.expiresAt).toISOString().slice(0, 10)}. Next: ${CMD} publish`,
52
44
  );
53
45
  return 0;
54
46
  } finally {
55
47
  rl?.close();
56
48
  }
57
- }),
49
+ }, args),
58
50
  };
@@ -3,12 +3,23 @@ import { logout, studioOrigin } from '../lib/publish/client.mjs';
3
3
 
4
4
  const usage = `Usage: xcore logout [--json]
5
5
 
6
- Sign this computer out of games.fun: the publish token is revoked at games.fun and forgotten on this computer. Your
6
+ Sign this computer out of xgames.fun: the publish token is revoked at xgames.fun and forgotten on this computer. Your
7
7
  published games stay published.`;
8
8
 
9
+ // signed out here, but not revoked at xgames.fun: why, and whether anything is left to do
10
+ const NOT_REVOKED = {
11
+ ended: o => `Signed out of ${o}; its token had already ended there (expired or revoked).`,
12
+ unreadable: o =>
13
+ `Forgot the sign-in on this computer, but this session could not read its token (it is kept where this session cannot reach, such as the Secret Service over SSH), so it was not revoked at ${o}. It ends by itself when it expires; revoke it from xgames.fun if you need it gone sooner.`,
14
+ offline: o =>
15
+ `Forgot the token on this computer, but ${o} did not answer, so it may still be live until it expires; revoke it from xgames.fun if you need to.`,
16
+ refused: o =>
17
+ `Forgot the token on this computer, but ${o} did not revoke it, so it may still be live until it expires; revoke it from xgames.fun if you need to.`,
18
+ };
19
+
9
20
  export default {
10
21
  name: 'logout',
11
- summary: 'sign this computer out of games.fun (revokes its publish token)',
22
+ summary: 'sign this computer out of xgames.fun (revokes its publish token)',
12
23
  usage,
13
24
  run: args =>
14
25
  guard(async () => {
@@ -22,8 +33,8 @@ export default {
22
33
  ? `This computer was not signed in to ${origin}.`
23
34
  : r.revoked
24
35
  ? `Signed out of ${origin}; the token is revoked.`
25
- : `Forgot the token on this computer, but ${origin} did not answer, so it may still be live until it expires; revoke it from games.fun if you need to.`,
36
+ : NOT_REVOKED[r.why](origin),
26
37
  );
27
38
  return 0;
28
- }),
39
+ }, args),
29
40
  };
package/commands/loot.mjs CHANGED
@@ -25,7 +25,7 @@ A game's economy on loot, one shared Solana program (devnet only until it is aud
25
25
  xcore loot help everything else xsolana does (its help says \`xsolana <command>\`: run each as
26
26
  \`xcore loot <command>\`)
27
27
  The file is ${ECONOMY_FILE} (loot.json in a game made before it was named so). Its RPC is XSOLANA_RPC_URL (in .env),
28
- else games.fun's shared devnet RPC. Published, the page uses the shared relay, which counts every call toward the game's
28
+ else xgames.fun's shared devnet RPC. Published, the page uses the shared relay, which counts every call toward the game's
29
29
  plan, unless the game brings its own RPC: ${CMD} add rpc (docs/SERVER.md, "The economy's own RPC").`;
30
30
 
31
31
  const ADD_USAGE = `Usage: xcore loot add [game] [--json]
@@ -62,7 +62,7 @@ export default {
62
62
  );
63
63
  for (const s of r.steps) console.log(` - ${s}`);
64
64
  return 0;
65
- });
65
+ }, args);
66
66
  const xsolana = solanaCommand();
67
67
  if (!xsolana) {
68
68
  console.error(
package/commands/plan.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // `xcore plan`: a game's backend plan on games.fun (../lib/publish/plans.mjs).
1
+ // `xcore plan`: a game's backend plan on xgames.fun (../lib/publish/plans.mjs).
2
2
  import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
3
3
  import { resolveGame } from '../lib/games.mjs';
4
4
  import { accountPlan, describeAccount, describeGame, gamePlan, setActive } from '../lib/publish/plans.mjs';
@@ -9,24 +9,25 @@ const usage = `Usage: xcore plan [dir] [--json]
9
9
  xcore plan activate [dir] [--json]
10
10
  xcore plan deactivate [dir] [--json]
11
11
 
12
- What a game's backend may use on games.fun (its server, saves and scores), and what it used this month.
12
+ What a game's backend may use on xgames.fun (its server, saves and scores), and what it used this month.
13
13
  Free every game, by default: a small server (50 ms of CPU a request, 600 requests a minute) and $1 of use a
14
14
  month; past that its server is capped (slow) until the month ends
15
- Creator a game in one of your paid game slots: 200 ms a request, 6,000 requests a minute, 1 GB of saves, and
16
- $10 of use a month per slot, shared by your Creator games
17
- Scale granted by games.fun for a game that needs more
15
+ Creator a game in one of your paid slots: 200 ms a request, 6,000 requests a minute, 1 GB of saves, and
16
+ $10 of use a month per paid slot, shared by your Creator games
17
+ Scale granted by xgames.fun for a game that needs more
18
18
  Every plan lets sites of yours embed the game (Free 3, Creator 10, Scale 25: ${CMD} embed); Creator and Scale serve
19
19
  it on domains of your own (1 and 5: ${CMD} domain).
20
- A slot is $15 for 30 days, prepaid in SOL or USDC on games.fun (Studio → Plans; no automatic renewal), and moves
21
- between your games at any time without changing the bill. Usage credit, bought the same way, lets Creator games go
22
- past what their slots include (at twice what games.fun pays) instead of slowing down.
23
- activate put this folder's game in a free slot
24
- deactivate take it out (it goes back to Free and the slot is free for another game)
25
- --all your slots and every game's plan`;
20
+ Every game sits in a slot. Everyone gets 2 free slots, whose games run on Free; any more games need a paid slot,
21
+ $15 for 30 days, prepaid in SOL or USDC on xgames.fun (your console → Plans; no automatic renewal), whose game runs on
22
+ Creator. A paid slot moves between your games at any time without changing the bill. Usage credit, bought the same way,
23
+ lets Creator games go past what their slots include (at twice what xgames.fun pays) instead of slowing down.
24
+ activate move this folder's game to a paid slot (its free slot opens for another game)
25
+ deactivate move it back to a free slot (one must be open; its paid slot opens for another game)
26
+ --all your free and paid slots and every game's plan`;
26
27
 
27
28
  export default {
28
29
  name: 'plan',
29
- summary: "a game's backend plan on games.fun (Free, Creator, Scale): its limits, this month's use, game slots",
30
+ summary: "a game's backend plan on xgames.fun (Free, Creator, Scale): its limits, this month's use, game slots",
30
31
  usage,
31
32
  run: args =>
32
33
  guard(async () => {
@@ -37,7 +38,7 @@ export default {
37
38
  const out = values.json ? printJson : null;
38
39
  const plain = e => {
39
40
  if (e?.status === 404)
40
- throw new Error(`this games.fun does not offer plans yet (${CMD} plan needs a newer games.fun).`);
41
+ throw new Error(`this xgames.fun does not offer plans yet (${CMD} plan needs a newer xgames.fun).`);
41
42
  throw e;
42
43
  };
43
44
  if (values.all) {
@@ -57,7 +58,9 @@ export default {
57
58
  const r = await setActive(dir, verb === 'activate').catch(e => {
58
59
  if (e?.status === 409)
59
60
  throw new Error(
60
- `${e.message} Free one with ${CMD} plan deactivate <another game>, or buy a slot on games.fun (Studio → Plans, in SOL or USDC).`,
61
+ e.code === 'no-free-slot'
62
+ ? `${e.message} Archive a game you no longer need on your console (xgames.fun/console), or move another game to a paid slot.`
63
+ : `${e.message} Open one with ${CMD} plan deactivate <another game>, or buy a paid slot on xgames.fun (your console → Plans, in SOL or USDC).`,
61
64
  );
62
65
  return plain(e);
63
66
  });
@@ -66,13 +69,13 @@ export default {
66
69
  const what =
67
70
  verb === 'activate'
68
71
  ? r.changed
69
- ? 'In a slot: Creator from its next request.'
70
- : 'Already in a slot.'
72
+ ? 'In a paid slot: Creator from its next request; its free slot is open for another game.'
73
+ : 'Already in a paid slot.'
71
74
  : r.changed
72
- ? 'Out of its slot: Free from its next request, and the slot is free.'
73
- : 'It was not in a slot.';
75
+ ? 'Back in a free slot: Free from its next request, and its paid slot is open for another game.'
76
+ : 'It was not in a paid slot.';
74
77
  console.log(`${what}\n${describeGame(r)}`);
75
78
  }
76
79
  return 0;
77
- }),
80
+ }, args),
78
81
  };
@@ -1,4 +1,4 @@
1
- // `xcore publish`: a game to games.fun at a link anyone can play (../lib/publish/index.mjs). publishCommand makes the
1
+ // `xcore publish`: a game to xgames.fun at a link anyone can play (../lib/publish/index.mjs). publishCommand makes the
2
2
  // command: this package's own, which checks a game by the page check, and the kit's (@xgamesfun/kit), which passes its
3
3
  // six gates, its name, its upkeep and its own way of finding a game. A game the kit made goes to the kit's (../bin).
4
4
  import { guard, parse, printJson } from '../lib/cli.mjs';
@@ -11,27 +11,27 @@ import { CMD } from '../lib/command.mjs';
11
11
 
12
12
  const usage = `Usage: xcore publish [dir] [--draft] [--new] [--title <name>] [--status] [--check [--verify [--port <n>]]] [--json]
13
13
 
14
- Put a game on games.fun at a link anyone can play: games.fun hosts it, you need no server. Sign in first
14
+ Put a game on xgames.fun at a link anyone can play: xgames.fun hosts it, you need no server. Sign in first
15
15
  (${CMD} login). The game's checks gate it: they must pass on the files you publish (they run now unless nothing
16
16
  changed since they last passed; a game you brought: the page check, a game the kit made: its six gates). A game whose
17
17
  checks fail goes up as a private draft only, and you are told which checks failed. Files that look like keys, .env
18
18
  files and links refuse it; evidence/, hidden files and the makers' notes at the top (README.md, NOTES.md, LAUNCH.md…)
19
- are left out. Only files games.fun does not have yet are uploaded. The link is unlisted: only people you give it to
19
+ are left out. Only files xgames.fun does not have yet are uploaded. The link is unlisted: only people you give it to
20
20
  find it. Publishing the same folder again makes a new version at the same link. A game with its own server code
21
- (server/, a gf-server-v1 Worker) sends it bundled as one file, when games.fun takes server code; server/ is never a
21
+ (server/, a gf-server-v1 Worker) sends it bundled as one file, when xgames.fun takes server code; server/ is never a
22
22
  public file.
23
23
  In a GitHub Actions job with permissions id-token: write, the job's own OIDC token signs it in place of a login.
24
24
  A brought game (${CMD} adopt) is built first when it has a build; its published folder (gamesfun.json page.out)
25
25
  goes up, with its server/ bundle when it has one.
26
- --draft a private version in your games.fun Studio, no public link
26
+ --draft a private version in your xgames.fun Studio, no public link
27
27
  --new publish as a new game (a new link), not an update of this folder's game
28
- --title <name> the game's name on games.fun (default: its title)
28
+ --title <name> the game's name on xgames.fun (default: its title)
29
29
  --status this folder's link and the account it is published from; publishes nothing
30
30
  --check the offline checks only (files, secrets, backend origins, the server bundle); publishes nothing
31
31
  --verify with --check: also run the publish gate's checks on these files and remember a pass, so the
32
32
  publish after it does not run them again (the GitHub Action's check step)
33
33
  --port <n> with --verify: the port its page check serves on (default: any free one)
34
- GAMESFUN_STUDIO=<origin> publishes to another games.fun (a local Studio while developing).`;
34
+ GAMESFUN_STUDIO=<origin> publishes to another xgames.fun (a local Studio while developing).`;
35
35
 
36
36
  /**
37
37
  * The publish command. check, label, prepare, rpcFile: as publishGame takes them (the kit's six gates, its name, its
@@ -49,7 +49,7 @@ export function publishCommand({
49
49
  const own = { check, label, ...(prepare ? { prepare } : {}), ...(rpcFile ? { rpcFile } : {}) };
50
50
  return {
51
51
  name: 'publish',
52
- summary: 'put a game on games.fun at a link anyone can play (unlisted)',
52
+ summary: 'put a game on xgames.fun at a link anyone can play (unlisted)',
53
53
  usage,
54
54
  run: args =>
55
55
  guard(async () => {
@@ -75,7 +75,13 @@ export function publishCommand({
75
75
  console.log(
76
76
  s.signedIn
77
77
  ? `Signed in to ${origin} as ${s.signedIn.email ?? s.signedIn.as}.`
78
- : `Not signed in to ${origin} (${CMD} login).`,
78
+ : s.signIn.oidcError
79
+ ? `GitHub's OIDC token could not be had: ${s.signIn.oidcError}`
80
+ : s.signIn.state === 'expired'
81
+ ? `The sign-in to ${origin} ended on ${String(s.signIn.expiresAt).slice(0, 10)} (${CMD} login).`
82
+ : s.signIn.state === 'unreadable'
83
+ ? `Signed in to ${origin} as ${s.signIn.email}, but this session cannot read the token (${CMD} login here).`
84
+ : `Not signed in to ${origin} (${CMD} login).`,
79
85
  );
80
86
  console.log(statusLine(named(dir), origin, s));
81
87
  }
@@ -126,6 +132,16 @@ export function publishCommand({
126
132
  newGame: values.new,
127
133
  title: values.title ?? null,
128
134
  progress: values.json ? () => {} : l => console.error(l),
135
+ }).catch(e => {
136
+ // A new game needs one of the account's 2 free slots (xgames.fun's words say so); name the commands that free one.
137
+ if (e?.code === 'no-free-slot')
138
+ throw Object.assign(
139
+ new Error(
140
+ `${e.message}\n ${CMD} plan activate <another game's folder> moves that game to a paid slot\n ${CMD} plan --all your slots and games; archive one on ${origin}/console`,
141
+ ),
142
+ { status: e.status, code: e.code },
143
+ );
144
+ throw e;
129
145
  });
130
146
  if (values.json) printJson(r);
131
147
  else
@@ -136,7 +152,7 @@ export function publishCommand({
136
152
  ).join('\n'),
137
153
  );
138
154
  return r.url || values.draft ? 0 : 1;
139
- }),
155
+ }, args),
140
156
  };
141
157
  }
142
158
 
@@ -148,20 +164,30 @@ const failedNames = failed => failed.map(c => `${c.n} ${c.name}`).join('; ');
148
164
 
149
165
  /** `publish --status`: where this folder's game is published, and whether it is live. */
150
166
  function statusLine(where, origin, s) {
151
- if (!s.known) return `${where} has not been published to ${origin} yet.`;
152
- const at = s.known.url ? `published at ${s.known.url}` : 'a private draft';
153
167
  let live = '';
154
- if (s.live?.error) live = ` games.fun says: ${s.live.error}`;
168
+ if (s.live?.error) live = ` xgames.fun says: ${s.live.error}`;
155
169
  else if (s.live) live = s.live.published ? ` Live: version ${s.live.version?.number}.` : ' Not live now.';
156
- const own = s.known.gameUrl ? `\n the game itself: ${s.known.gameUrl}` : '';
157
- return `${where} is ${at} (version ${s.known.lastVersion}).${live}${own}`;
170
+ const own = s.known?.gameUrl ? `\n the game itself: ${s.known.gameUrl}` : '';
171
+ const v = s.known?.lastVersion;
172
+ switch (s.link) {
173
+ case 'never':
174
+ return `${where} has not been published to ${origin} yet.`;
175
+ case 'started':
176
+ return `${where} was started on ${origin}, but no version finished going up. Run ${CMD} publish again: it goes on with the same game.${live}`;
177
+ case 'draft':
178
+ return `${where} is a private draft (version ${v}).${live}${own}`;
179
+ case 'draft-over-public':
180
+ return `${where} is published at ${s.known.url}; its latest version (${v}) is a private draft, so an earlier version is the one playing.${live}${own}`;
181
+ default:
182
+ return `${where} is published at ${s.known.url} (version ${v}).${live}${own}`;
183
+ }
158
184
  }
159
185
 
160
186
  /** `publish --check` when nothing is refused: what goes up, and the server bundle when there is one. */
161
187
  function checkLines(where, out, p) {
162
188
  const talks = p.backend.length ? ` It may talk to: ${p.backend.join(', ')}.` : '';
163
189
  const lines = [
164
- `${where}: ${out.files} files${out.added?.length ? ` (with the ${plural(out.added.length, 'record')} games.fun keeps: ${out.added.join(', ')})` : ''}, ${(out.bytes / 1048576).toFixed(1)} MB, nothing refused.${talks} ` +
190
+ `${where}: ${out.files} files${out.added?.length ? ` (with the ${plural(out.added.length, 'record')} xgames.fun keeps: ${out.added.join(', ')})` : ''}, ${(out.bytes / 1048576).toFixed(1)} MB, nothing refused.${talks} ` +
165
191
  `Left out: ${out.skipped.join(', ') || 'nothing'}.`,
166
192
  ];
167
193
  const sv = out.server;
@@ -171,9 +197,16 @@ function checkLines(where, out, p) {
171
197
  ...(sv.compatibilityFlags.length ? [sv.compatibilityFlags.join(', ')] : []),
172
198
  ];
173
199
  lines.push(
174
- ` server code: ${sv.entry} bundled from ${plural(sv.files.length, 'file')} (${sv.files.join(', ')}) ` +
175
- `into one ${kb(sv.bytes)} file, sha256 ${sv.sha256.slice(0, 12)}…${extras.map(e => `, ${e}`).join('')}; ` +
176
- 'server/ and gamesfun.json are not public files',
200
+ sv.stock
201
+ ? ` server code: xcore's stock server (${kb(sv.bytes)}, sha256 ${sv.sha256.slice(0, 12)}…), which serves the page's api/db/ from the game's database under gamesfun.json → database.collections`
202
+ : ` server code: ${sv.entry} bundled from ${plural(sv.files.length, 'file')} (${sv.files.join(', ')}) ` +
203
+ `into one ${kb(sv.bytes)} file, sha256 ${sv.sha256.slice(0, 12)}…${extras.map(e => `, ${e}`).join('')}; ` +
204
+ 'server/ and gamesfun.json are not public files',
205
+ ...(sv.database
206
+ ? [
207
+ ` database: env.${sv.database.binding}, ${sv.database.migrations.length ? `${plural(sv.database.migrations.length, 'migration')} (${sv.database.migrations.join(', ')})` : 'no migrations'}`,
208
+ ]
209
+ : []),
177
210
  ...sv.warnings.map(w => ` ${w}`),
178
211
  );
179
212
  }
@@ -198,7 +231,7 @@ function publishedLines(where, r) {
198
231
  ` the game itself: ${r.gameUrl}`,
199
232
  ];
200
233
  if (r.friendsUrl) lines.push(` play with friends: ${r.friendsUrl}`);
201
- lines.push(` uploaded ${r.uploaded} new files; games.fun already had ${r.alreadyThere}.`);
234
+ lines.push(` uploaded ${r.uploaded} new files; xgames.fun already had ${r.alreadyThere}.`);
202
235
  if (r.server) {
203
236
  lines.push(
204
237
  r.server.sent
@@ -209,7 +242,7 @@ function publishedLines(where, r) {
209
242
  return lines;
210
243
  }
211
244
 
212
- /** A publish saved as a private draft: asked for with --draft, refused by games.fun, or its checks failed. */
245
+ /** A publish saved as a private draft: asked for with --draft, refused by xgames.fun, or its checks failed. */
213
246
  function draftLines(where, r, folder = where) {
214
247
  const lines = [`Saved ${where} as a private draft (version ${r.version.number}): ${r.studioUrl}`];
215
248
  if (r.publishError) lines.push(` not published: ${r.publishError}`);
@@ -1,4 +1,4 @@
1
- // `xcore secret set|list|delete`: a game server's own secrets on games.fun (../lib/publish/secrets.mjs).
1
+ // `xcore secret set|list|delete`: a game server's own secrets on xgames.fun (../lib/publish/secrets.mjs).
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
4
4
  import { resolveGame, shown } from '../lib/games.mjs';
@@ -11,12 +11,12 @@ const usage = `Usage: xcore secret set <NAME> [dir] [--from-file <path> | --from
11
11
  xcore secret delete <NAME> [dir] [--json]
12
12
 
13
13
  A key your game's server needs and no player may see (a voucher-signing key, an outside API's key, an RPC URL with
14
- its key): the server reads it as env.<NAME>. games.fun keeps the value only on the server's host (Cloudflare), never
14
+ its key): the server reads it as env.<NAME>. xgames.fun keeps the value only on the server's host (Cloudflare), never
15
15
  shows it again and never gives it to the page. Set it after the game is published with its server/; it reaches the
16
16
  server's next request, with no publish. On this machine, ${CMD} dev reads the same names from the game folder's
17
17
  .dev.vars (NAME=value lines; a hidden file, so publish never sends it; keep it out of git).
18
18
  set <NAME> the value from --from-file, or the folder's .dev.vars (--from-dev-vars: the same key as under
19
- xcore dev), or --new-key: a new Ed25519 key made here and sent to games.fun only, never kept on
19
+ xcore dev), or --new-key: a new Ed25519 key made here and sent to xgames.fun only, never kept on
20
20
  this machine (it prints the key's address: a voucher key's goes in the grant), else from stdin
21
21
  when it is piped, else typed (not shown)
22
22
  list the names (values are never shown)
@@ -71,7 +71,7 @@ async function fromDevVars(dir, name) {
71
71
 
72
72
  export default {
73
73
  name: 'secret',
74
- summary: "a game server's secrets on games.fun (env.<NAME>): set, list, delete; never the page's",
74
+ summary: "a game server's secrets on xgames.fun (env.<NAME>): set, list, delete; never the page's",
75
75
  usage,
76
76
  run: args =>
77
77
  guard(async () => {
@@ -129,9 +129,9 @@ export default {
129
129
  );
130
130
  if (made)
131
131
  console.log(
132
- `It is a new key, made here and kept only on games.fun's server. Its address: ${made.address}\nA voucher key's address goes in the grant's "vouchers" in xsolana.json (then ${CMD} loot approve).`,
132
+ `It is a new key, made here and kept only on xgames.fun's server. Its address: ${made.address}\nA voucher key's address goes in the grant's "vouchers" in xsolana.json (then ${CMD} loot approve).`,
133
133
  );
134
134
  }
135
135
  return 0;
136
- }),
136
+ }, args),
137
137
  };