@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
package/bin/xcore.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // The `xcore` command (@xgamesfun/xcore): games.fun's own (sign in, bring a game, check it as the host serves it, its
2
+ // The `xcore` command (@xgamesfun/xcore): xgames.fun's own (sign in, bring a game, check it as the host serves it, its
3
3
  // backend and economy, publish). Each command is a module in ../commands/<name>.mjs exporting `default { name,
4
4
  // summary, usage, run(args) }`, and optionally `helpFor(args)`, the --help text of a subcommand. Everything else is the
5
5
  // kit's (@xgamesfun/kit: create, verify, the build loop and the rest): when the kit is here, those go to it as
@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process';
9
9
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
10
10
  import { readdir } from 'node:fs/promises';
11
11
  import path from 'node:path';
12
+ import { parseArgs } from 'node:util';
12
13
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
14
  import { CMD } from '../lib/command.mjs';
14
15
 
@@ -39,9 +40,14 @@ export function findKit({ cwd = process.cwd(), env = process.env, here = HERE }
39
40
  return null;
40
41
  }
41
42
 
43
+ // the flags of check, publish, dev and backend that take a value: their value is never the game's folder
44
+ const VALUE_FLAGS = Object.fromEntries(
45
+ ['out', 'port', 'title', 'url', 'origin'].map(f => [f, { type: 'string', multiple: true }]),
46
+ );
47
+
42
48
  /** Does this command, with these arguments, belong to the kit even though this package has a command by its name? */
43
- function kitsTurn(name, args) {
44
- const positionals = args.filter(a => !a.startsWith('-'));
49
+ export function kitsTurn(name, args) {
50
+ const { positionals } = parseArgs({ args, options: VALUE_FLAGS, strict: false, allowPositionals: true });
45
51
  if (name === 'backend') return !(positionals[0] === 'use' && positionals[1] === 'custom');
46
52
  if (name === 'publish' || name === 'check' || name === 'dev') {
47
53
  const dir = path.resolve(positionals[0] ?? '.');
@@ -72,12 +78,11 @@ function runKit(kit, args) {
72
78
  });
73
79
  }
74
80
 
75
- const NO_KIT = `That is the kit's (@xgamesfun/kit): it makes games, the studio page, verify and the build loop. Get it with
76
- git clone https://github.com/XGamesFun/games-kit && cd games-kit && npm ci
77
- then run xcore inside that folder, or set GAMESFUN_KIT=<folder> to use a kit from anywhere.`;
81
+ const NO_KIT = `That is the kit's (@xgamesfun/kit): it makes games, the studio page, verify and the build loop. The kit is
82
+ not released yet; with a checkout of it, run xcore inside that folder, or set GAMESFUN_KIT=<folder> to use it from anywhere.`;
78
83
 
79
84
  function help(commands, kit) {
80
- const lines = ['Usage: xcore <command> [options]', '', 'games.fun:'];
85
+ const lines = ['Usage: xcore <command> [options]', '', 'xgames.fun:'];
81
86
  for (const c of commands.values()) lines.push(` ${c.name.padEnd(10)} ${c.summary}`);
82
87
  lines.push(
83
88
  '',
@@ -119,7 +124,7 @@ export async function main([name, ...args]) {
119
124
  console.error(
120
125
  own
121
126
  ? `${NO_KIT}\n\n${own.usage ?? ''}`
122
- : `"${name}" is not a games.fun command. ${NO_KIT}\n\n${help(commands, null)}`,
127
+ : `"${name}" is not an xgames.fun command. ${NO_KIT}\n\n${help(commands, null)}`,
123
128
  );
124
129
  return 1;
125
130
  }
package/commands/add.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // `xcore add signin|database|multiplayer|rpc|vouchers [folder]`: games.fun's player sign-in, saves and leaderboards, a database of
1
+ // `xcore add signin|database|multiplayer|rpc|vouchers [folder]`: xgames.fun's player sign-in, saves and leaderboards, a database of
2
2
  // the game's own, multiplayer rooms, the economy's own Solana RPC or its own voucher key, put into a web game, a Godot 4 project or a Unity project (../lib/add.mjs).
3
3
  import path from 'node:path';
4
4
  import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
@@ -7,29 +7,32 @@ import { addDatabase, addMultiplayer, addRpc, addSignin, addVouchers } from '../
7
7
 
8
8
  const usage = `Usage: xcore add signin|database|multiplayer|rpc|vouchers [folder] [--json]
9
9
 
10
- signin games.fun's player sign-in (X first, or a wallet), each player's saves and leaderboards, from the game's
10
+ signin xgames.fun's player sign-in (X first, or a wallet), each player's saves and leaderboards, from the game's
11
11
  own code. A web game gets gamesfun.js beside its page; a Godot 4 project an autoload (GamesFun); a Unity
12
12
  project a WebGL plugin (Assets/Plugins/GamesFun). On this machine a local player stands in.
13
- database all of signin, and a database of the game's own: server code (server/database.js, SQLite on games.fun)
14
- that publish puts up with the game. Documents in collections; who may read and write each is in its RULES.
13
+ database all of signin, and a database of the game's own (one D1 database a game, made on xgames.fun by the
14
+ first publish: gamesfun.json → database). With a server of its own, its code reads and writes it
15
+ (server/gf-db.js; tables from server/migrations/); with none, xcore's stock server keeps the page's
16
+ documents in it (gf.db), each player's own, under gamesfun.json → database.collections. xcore db
17
+ queries, exports and restores it.
15
18
  multiplayer
16
19
  all of signin, and rooms of the game's own: server code (server/rooms.js) that relays between the players
17
20
  of a room, the first in its host. Godot's high-level multiplayer (RPCs, spawners, synchronizers) runs over
18
21
  it unchanged (GamesFun.join_room); Unity and web games get a room with send and events.
19
22
  rpc the economy's Solana calls through the game's own devnet RPC (server/rpc.js at api/rpc, its URL a
20
- secret of the server: xcore secret set XSOLANA_RPC_URL), not games.fun's shared relay, which counts
23
+ secret of the server: xcore secret set XSOLANA_RPC_URL), not xgames.fun's shared relay, which counts
21
24
  every call toward the game's plan.
22
25
  vouchers the game's own voucher key, so its server pays the results it checked from a grant in the economy
23
26
  (server/vouchers.js at api/vouchers/: PAY.check decides; a Durable Object pays each result once). Makes
24
27
  a test key (its seed in .dev.vars; the live key: xcore secret set VOUCHER_SEED --new-key) and says its
25
- address for the grant. For prizes by place that games.fun's keeper can sign instead, docs/SERVER.md.
28
+ address for the grant. For prizes by place that xgames.fun's keeper can sign instead, docs/SERVER.md.
26
29
 
27
30
  Run it again to bring the files it wrote up to date. The folder defaults to the one you are in.`;
28
31
 
29
32
  export default {
30
33
  name: 'add',
31
34
  summary:
32
- "give a web, Godot or Unity game games.fun's sign-in, saves and leaderboards, a database, multiplayer, its own RPC or voucher key",
35
+ "give a web, Godot or Unity game xgames.fun's sign-in, saves and leaderboards, a database, multiplayer, its own RPC or voucher key",
33
36
  usage,
34
37
  run: args =>
35
38
  guard(async () => {
@@ -51,5 +54,5 @@ export default {
51
54
  for (const w of r.written.length ? r.written : ['already up to date']) console.log(` ${w}`);
52
55
  console.log('\nNext:');
53
56
  for (const s of r.steps) console.log(` - ${s}`);
54
- }),
57
+ }, args),
55
58
  };
@@ -1,9 +1,9 @@
1
- // `xcore adopt [folder]`: bring a web game made anywhere, to check it, add an economy and publish it on games.fun
1
+ // `xcore adopt [folder]`: bring a web game made anywhere, to check it, add an economy and publish it on xgames.fun
2
2
  // (../lib/publish/adopt.mjs; README.md).
3
3
  import { guard, parse, printJson } from '../lib/cli.mjs';
4
4
  import path from 'node:path';
5
5
  import { shown } from '../lib/games.mjs';
6
- import { adopt, reportLines } from '../lib/publish/adopt.mjs';
6
+ import { adopt, adoptNext, reportLines } from '../lib/publish/adopt.mjs';
7
7
  import { CMD } from '../lib/command.mjs';
8
8
 
9
9
  const usage = `Usage: xcore adopt [folder] [--build "<command>"] [--out <dir>] [--static <dir>] [--no-build] [--json]
@@ -11,7 +11,7 @@ const usage = `Usage: xcore adopt [folder] [--build "<command>"] [--out <dir>] [
11
11
  Bring a web game you already made (plain static files, or a project whose build makes them: Vite, Next.js static
12
12
  export, Astro, SvelteKit's static adapter, Nuxt generate, Create React App, Parcel, Gatsby, any build script). It reads
13
13
  the folder's shape, builds it, writes its record (gamesfun.json → page: how it builds and what is published; never
14
- published itself) and lists everything that does not fit games.fun's hosting, each with its fix: no static index.html
14
+ published itself) and lists everything that does not fit xgames.fun's hosting, each with its fix: no static index.html
15
15
  (server-side rendering, a Node server as the frontend), secrets, .env files, links, files over the limits, paths from
16
16
  the root, scripts, styles, fonts or frames from other sites, sites the code names that backend.json does not list.
17
17
  Then: ${CMD} check (the page in Chrome as the host serves it), ${CMD} backend use custom (a backend you
@@ -27,7 +27,7 @@ The folder (default: the one you are in) may be anywhere: in games/ or outside i
27
27
 
28
28
  export default {
29
29
  name: 'adopt',
30
- summary: 'bring a web game you already made: its record, and what to fix for games.fun hosting',
30
+ summary: 'bring a web game you already made: its record, and what to fix for xgames.fun hosting',
31
31
  usage,
32
32
  run: args =>
33
33
  guard(async () => {
@@ -49,9 +49,11 @@ export default {
49
49
  noBuild: values['no-build'] === true,
50
50
  onLine: values.json ? () => {} : l => console.error(` build: ${l}`),
51
51
  });
52
- if (values.json) printJson({ ...r, dir: shown(r.dir) });
53
- else console.log(reportLines(r, { shown }).join('\n'));
52
+ if (values.json) {
53
+ const { state, next } = adoptNext(r, { shown });
54
+ printJson({ ...r, dir: shown(r.dir), state, next });
55
+ } else console.log(reportLines(r, { shown }).join('\n'));
54
56
  // 1 while anything is refused or left to fix (for scripts and agents); 0 when only "worth a look" remains
55
57
  return r.refused.length || r.fix.length ? 1 : 0;
56
- }),
58
+ }, args),
57
59
  };
@@ -1,5 +1,5 @@
1
1
  // `xcore backend use custom`: a backend you already run on the public internet (an API, a WebSocket server). It
2
- // writes backend.json with its URL and the origins the page talks to, so games.fun's play host lets the published page
2
+ // writes backend.json with its URL and the origins the page talks to, so xgames.fun's play host lets the published page
3
3
  // reach them (its page policy opens to exactly those). Nothing is signed in to or deployed; its keys stay on its
4
4
  // server. A backend the kit connects for you (Supabase, Railway) is the kit's: `backend connect` with @xgamesfun/kit.
5
5
  import { mkdir, writeFile } from 'node:fs/promises';
@@ -66,5 +66,5 @@ export default {
66
66
  `Wrote ${shown(file)}: the published page may reach ${config.origins.join(', ')}. Next: ${CMD} check, which fails on a request to any other origin.`,
67
67
  );
68
68
  return 0;
69
- }),
69
+ }, args),
70
70
  };
@@ -1,4 +1,4 @@
1
- // `xcore check [dir]`: the page check (../lib/check.mjs) on a game you brought: it builds, fits games.fun's hosting,
1
+ // `xcore check [dir]`: the page check (../lib/check.mjs) on a game you brought: it builds, fits xgames.fun's hosting,
2
2
  // starts clean in Chrome as the play host serves it, and stays inside its origins. publish runs the same check first.
3
3
  import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
4
4
  import { resolveGame, shown } from '../lib/games.mjs';
@@ -7,7 +7,7 @@ import { CMD } from '../lib/command.mjs';
7
7
 
8
8
  const usage = `Usage: xcore check [dir] [--no-build] [--out <dir>] [--port <n>] [--json]
9
9
 
10
- The page check, as games.fun hosts the game: 1 it builds (gamesfun.json page.build), 2 it fits the hosting (what
10
+ The page check, as xgames.fun hosts the game: 1 it builds (gamesfun.json page.build), 2 it fits the hosting (what
11
11
  publish would refuse, and what breaks once hosted: paths from the root, scripts, styles, fonts or frames from other
12
12
  sites), 3 it starts clean in Chrome at a phone's size and a desktop's under the play host's page policy, 4 it stays
13
13
  inside its origins (its own files, the host's own paths, backend.json's origins). It does not check that the game plays
@@ -19,7 +19,7 @@ as it should: that is your own game's tests. Run ${CMD} adopt <dir> first.
19
19
 
20
20
  export default {
21
21
  name: 'check',
22
- summary: 'the page check: it builds, fits games.fun hosting, starts clean, stays inside its origins',
22
+ summary: 'the page check: it builds, fits xgames.fun hosting, starts clean, stays inside its origins',
23
23
  usage,
24
24
  run: args =>
25
25
  guard(async () => {
@@ -56,5 +56,5 @@ export default {
56
56
  );
57
57
  }
58
58
  return r.pass ? 0 : 1;
59
- }),
59
+ }, args),
60
60
  };
@@ -0,0 +1,218 @@
1
+ // `xcore db info|query|export|import|restore`: the game's own database (gamesfun.json → database), on this machine
2
+ // (the one `xcore dev` runs) or, with --live, on xgames.fun (../lib/db.mjs).
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
5
+ import { resolveGame, shown } from '../lib/games.mjs';
6
+ import { dbExport, dbImport, dbInfo, dbQuery, dbRestore, importOf, queryProblem } from '../lib/db.mjs';
7
+ import { economyFile } from '../lib/economy.mjs';
8
+ import { pageRoots } from '../lib/publish/page.mjs';
9
+ import { CMD } from '../lib/command.mjs';
10
+
11
+ const usage = `Usage: xcore db info [dir] [--live] [--json]
12
+ xcore db query "<sql>" [dir] [--params '<json list>'] [--write] [--live [--yes]] [--json]
13
+ xcore db export [dir] [--out <file>] [--live] [--json]
14
+ xcore db import <file.sql> [dir] [--live [--yes]] [--json]
15
+ xcore db restore <time> [dir] --live [--yes] [--json]
16
+
17
+ The game's own database (${CMD} add database: gamesfun.json → database, one D1 database a game). Without --live,
18
+ the one on this machine that ${CMD} dev runs (kept in .gamesfun/dev/d1/, its migrations applied first); with --live,
19
+ the game's database on xgames.fun, which keeps every call in the game's audit log.
20
+ info where it is, its size, the migrations applied (and, here, its tables and their rows)
21
+ query "<sql>" one statement; it only reads (SELECT, EXPLAIN, PRAGMA table_info or table_list) unless
22
+ --write. --params '["moon", 3]' fills its ? marks
23
+ export the whole database as SQL, into --out <file> or .gamesfun/exports/ (hidden: never published;
24
+ it holds your players' data, so keep it out of git and off shared drives)
25
+ import <file.sql> run a file of SQL statements, all of it or none; at most 5 MB (live, 4.5 MB as sent). A dump
26
+ from export (here, live or D1's) goes in over what is there, or into an empty database:
27
+ tables kept, each row updated by its key where it is (none deleted)
28
+ A change to the live database (query --write --live, import --live, restore) goes on only with --yes or when you
29
+ type its word (write, import, restore); where there is no terminal to ask in, it needs --yes.
30
+ restore <time> live only: the database as it was at <time> (a date such as 2026-11-08T12:00:00Z, or unix
31
+ seconds; up to 30 days back). It says what it would go back to first, and goes on only with
32
+ --yes or when you type "restore". A game with an economy: what happened on chain (loot
33
+ balances, payouts) stays as it is, so the database and the chain can disagree after it.`;
34
+
35
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
36
+ const kb = n => (n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : `${(n / 1024).toFixed(1)} KB`);
37
+
38
+ /** Rows as columns a person reads; long values cut short. */
39
+ function table(rows) {
40
+ if (!rows.length) return '(no rows)';
41
+ const cols = [...new Set(rows.flatMap(r => Object.keys(r)))];
42
+ const cell = v => {
43
+ const s = v === null || v === undefined ? 'NULL' : typeof v === 'object' ? JSON.stringify(v) : String(v);
44
+ return s.length > 60 ? `${s.slice(0, 59)}…` : s;
45
+ };
46
+ const width = cols.map(c => Math.max(c.length, ...rows.map(r => cell(r[c]).length)));
47
+ const line = vals =>
48
+ vals
49
+ .map((v, i) => v.padEnd(width[i]))
50
+ .join(' ')
51
+ .trimEnd();
52
+ return [line(cols), line(width.map(w => '-'.repeat(w))), ...rows.map(r => line(cols.map(c => cell(r[c]))))].join(
53
+ '\n',
54
+ );
55
+ }
56
+
57
+ /**
58
+ * A change to the live database goes on only with --yes, or when the person types `word` at the terminal; with no
59
+ * terminal to ask in (a script, CI), it is refused. → true to go on
60
+ */
61
+ async function confirmed(question, word, nothing) {
62
+ if (!process.stdin.isTTY)
63
+ throw new UsageError(`${nothing}: add --yes to go on (this is not a terminal to ask in)\n\n${usage}`);
64
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
65
+ try {
66
+ return (await rl.question(question)).trim().toLowerCase() === word;
67
+ } finally {
68
+ rl.close();
69
+ }
70
+ }
71
+
72
+ export default {
73
+ name: 'db',
74
+ summary: "the game's own database (D1): info, query, export, import, restore; here, or --live on xgames.fun",
75
+ usage,
76
+ run: args =>
77
+ guard(async () => {
78
+ const { values, positionals } = parse(
79
+ args,
80
+ {
81
+ live: { type: 'boolean' },
82
+ write: { type: 'boolean' },
83
+ params: { type: 'string' },
84
+ out: { type: 'string' },
85
+ yes: { type: 'boolean' },
86
+ },
87
+ usage,
88
+ );
89
+ const [verb, ...rest] = positionals;
90
+ const takes = { query: 'a statement', import: 'a file', restore: 'a time' };
91
+ if (!['info', 'query', 'export', 'import', 'restore'].includes(verb)) throw new UsageError(usage);
92
+ if (takes[verb] && !rest[0]) throw new UsageError(`${verb} needs ${takes[verb]}\n\n${usage}`);
93
+ const arg = takes[verb] ? rest[0] : null;
94
+ const dir = await resolveGame(takes[verb] ? rest[1] : rest[0]);
95
+ const live = !!values.live;
96
+ const where = live ? `${shown(dir)}'s database on xgames.fun` : `${shown(dir)}'s database on this machine`;
97
+ const log = m => console.error(m);
98
+
99
+ if (verb === 'info') {
100
+ const r = await dbInfo(dir, { live, log });
101
+ if (values.json) return printJson(r);
102
+ if (live && !r.exists) {
103
+ console.log(`${shown(dir)} has no database on xgames.fun yet: it is made on the first publish with one.`);
104
+ return 0;
105
+ }
106
+ console.log(
107
+ `${where}${r.name ? ` (${r.name})` : ''}: ${kb(r.sizeBytes ?? 0)}${r.cap ? ` of ${kb(r.cap)}` : ''}${r.folder ? `, in ${r.folder}/` : ''}`,
108
+ );
109
+ const applied = r.applied ?? [];
110
+ console.log(` migrations applied: ${applied.length ? applied.map(m => m.name ?? m).join(', ') : 'none'}`);
111
+ if (r.tables)
112
+ console.log(
113
+ ` tables: ${r.tables.length ? r.tables.map(t => `${t.name} (${plural(t.rows, 'row')})`).join(', ') : 'none yet'}`,
114
+ );
115
+ return 0;
116
+ }
117
+ if (verb === 'query') {
118
+ let params = [];
119
+ if (values.params !== undefined) {
120
+ try {
121
+ params = JSON.parse(values.params);
122
+ } catch {
123
+ throw new UsageError(`--params is a JSON list, such as '["moon", 3]'\n\n${usage}`);
124
+ }
125
+ }
126
+ if (live && values.write && !values.yes) {
127
+ // what would be refused anyway is refused before anyone is asked
128
+ const why = queryProblem(arg, { write: true });
129
+ if (why) throw Object.assign(new Error(why), { code: 'bad-sql' });
130
+ console.error(
131
+ `This changes ${where}, which your players use: ${arg.length > 200 ? `${arg.slice(0, 199)}…` : arg}. Only ${CMD} db restore --live puts it back.`,
132
+ );
133
+ if (!(await confirmed('Type write to go on, anything else to stop: ', 'write', 'nothing was written'))) {
134
+ console.log('Nothing was written.');
135
+ return 1;
136
+ }
137
+ }
138
+ const r = await dbQuery(dir, arg, { live, params, write: !!values.write, log });
139
+ if (values.json) return printJson(r);
140
+ console.log(table(r.results ?? []));
141
+ const m = r.meta ?? {};
142
+ const changed = m.changes ?? m.rows_written;
143
+ console.log(
144
+ `${plural((r.results ?? []).length, 'row')}${changed ? `; ${plural(changed, 'row')} changed` : ''} (${where})`,
145
+ );
146
+ return 0;
147
+ }
148
+ if (verb === 'export') {
149
+ const r = await dbExport(dir, { live, out: values.out ?? null, log });
150
+ if (values.json) return printJson(r);
151
+ console.log(`Exported ${where} (${kb(r.bytes)}) to ${r.file}. It holds your players' data: keep it private.`);
152
+ if (r.note) console.log(` ${r.note}`);
153
+ return 0;
154
+ }
155
+ if (verb === 'import') {
156
+ if (live && !values.yes) {
157
+ // what would be refused anyway (not there, too big to send) is refused before anyone is asked
158
+ await importOf(arg, { live });
159
+ console.error(
160
+ `This runs ${arg} on ${where}, which your players use: rows can change or go, and while it runs the database answers nothing else. ${CMD} db export --live keeps a copy first; ${CMD} db restore --live puts it back.`,
161
+ );
162
+ if (!(await confirmed('Type import to go on, anything else to stop: ', 'import', 'nothing was imported'))) {
163
+ console.log('Nothing was imported.');
164
+ return 1;
165
+ }
166
+ }
167
+ const r = await dbImport(dir, arg, { live, log });
168
+ if (values.json) return printJson(r);
169
+ console.log(
170
+ `Imported ${arg} into ${where}${r.meta?.queries !== undefined ? ` (${plural(r.meta.queries, 'statement')})` : ''}${r.statements !== undefined ? ` (${plural(r.statements, 'statement')})` : ''}.${r.dump ? ' A dump goes in over what is there: its tables kept, each row updated by its key where it is, none deleted.' : ''}${r.left ? ` Its record of migrations (_gf_migrations, ${plural(r.left, 'statement')}) was left out: this database keeps its own.` : ''}`,
171
+ );
172
+ if (r.note) console.log(` ${r.note}`);
173
+ return 0;
174
+ }
175
+ // restore: what it would do, the chain's warning, then only on a yes
176
+ if (!live)
177
+ throw new UsageError(
178
+ `restore is --live only: the database on this machine has no history (${CMD} db import a dump from ${CMD} db export)\n\n${usage}`,
179
+ );
180
+ const p = await dbRestore(dir, arg, { live, log });
181
+ const roots = pageRoots(dir);
182
+ const economy = !!(p.economy || economyFile(roots.static ?? dir) || economyFile(dir));
183
+ const at = p.preview?.at ?? arg;
184
+ const warning = economy
185
+ ? 'This game has an economy: what happened on chain since then stays as it is. Loot balances, items and payouts are not rolled back, so a player can keep what they won while the database forgets it (or the other way round).'
186
+ : null;
187
+ if (!values.yes) {
188
+ if (values.json)
189
+ return printJson({ ...p, economy, restored: null, next: `${CMD} db restore ${arg} --live --yes` });
190
+ console.log(
191
+ `${where} would go back to how it was at ${at}${p.preview?.bookmark ? ` (bookmark ${p.preview.bookmark})` : ''}. Everything written after that is lost; ${CMD} db export --live keeps a copy first.`,
192
+ );
193
+ const undone = p.preview?.migrations ?? [];
194
+ if (undone.length)
195
+ console.log(
196
+ `It rolls back ${plural(undone.length, 'migration')} applied after that time: ${undone.map(m => `${m.name}${m.at ? ` (${m.at})` : ''}`).join(', ')}.`,
197
+ );
198
+ if (p.preview?.warning) console.log(`Warning: ${p.preview.warning}`);
199
+ if (warning) console.log(`Warning: ${warning}`);
200
+ if (!(await confirmed('Type restore to go on, anything else to stop: ', 'restore', 'nothing was restored'))) {
201
+ console.log('Nothing was restored.');
202
+ return 1;
203
+ }
204
+ } else if (warning && !values.json) console.error(`Warning: ${warning}`);
205
+ // the moment before the restore: going back to it undoes the restore (D1 Time Travel keeps that point too)
206
+ const before = new Date().toISOString();
207
+ const r = await dbRestore(dir, arg, { live, confirm: true, log });
208
+ const undo = `${CMD} db restore ${before} --live`;
209
+ if (values.json) return printJson({ ...r, economy, undo: { at: before, command: undo } });
210
+ console.log(
211
+ `Restored ${where} to ${r.restored?.at ?? at}${r.restored?.bookmark ? ` (bookmark ${r.restored.bookmark})` : ''}.`,
212
+ );
213
+ console.log(
214
+ ` Before it, the database was at ${r.restored?.previous ? `bookmark ${r.restored.previous}` : before}. To undo this restore: ${undo} (the moment before it, by this machine's clock).`,
215
+ );
216
+ return 0;
217
+ }, args),
218
+ };
package/commands/dev.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // `xcore dev [dir]`: the game as games.fun hosts it, on this machine, with its server, sign-in, saves, rooms and
1
+ // `xcore dev [dir]`: the game as xgames.fun hosts it, on this machine, with its server, sign-in, saves, rooms and
2
2
  // economy RPC (../lib/dev/index.mjs). Runs until Ctrl+C.
3
3
  import { guard, parse, printJson, UsageError } from '../lib/cli.mjs';
4
4
  import { resolveGame, shown } from '../lib/games.mjs';
@@ -7,11 +7,11 @@ import { CMD } from '../lib/command.mjs';
7
7
 
8
8
  const usage = `Usage: xcore dev [dir] [--port <n>] [--no-build] [--open] [--json]
9
9
 
10
- Play the game on this machine as games.fun will host it, with everything a published game has behind it:
10
+ Play the game on this machine as xgames.fun will host it, with everything a published game has behind it:
11
11
  http://127.0.0.1:<port>/game/ the published files, with the play host's headers
12
12
  /game/api/* the game's own server/ (Durable Objects, WebSockets, env.GF), reloaded on change
13
13
  /platform/v1/* sign-in (a local player), saves and leaderboards, the same store env.GF uses
14
- /loot/v1/rpc the economy's Solana devnet RPC (XSOLANA_RPC_URL in .env, else games.fun's relay)
14
+ /loot/v1/rpc the economy's Solana devnet RPC (XSOLANA_RPC_URL in .env, else xgames.fun's relay)
15
15
  gamesfun.js (${CMD} add signin|database|multiplayer) uses all of it here, from any engine: open the page in two
16
16
  windows to play a room together. Running server/ needs Miniflare (workerd), installed once on first use (~140 MB).
17
17
  Saves, scores and Durable Object storage are kept in <dir>/.gamesfun/ (never published).
@@ -22,7 +22,7 @@ Saves, scores and Durable Object storage are kept in <dir>/.gamesfun/ (never pub
22
22
 
23
23
  export default {
24
24
  name: 'dev',
25
- summary: 'play the game here as games.fun hosts it: its server, sign-in, saves, rooms and economy RPC',
25
+ summary: 'play the game here as xgames.fun hosts it: its server, sign-in, saves, rooms and economy RPC',
26
26
  usage,
27
27
  run: args =>
28
28
  guard(async () => {
@@ -51,11 +51,13 @@ export default {
51
51
  throw new Error(`port ${port} is in use: pick another with --port <n>`, { cause: e });
52
52
  throw e;
53
53
  }
54
- if (values.json) printJson({ url: dev.url, origin: dev.origin, server: dev.server, rpc: dev.rpc });
54
+ if (values.json)
55
+ printJson({ url: dev.url, origin: dev.origin, files: dev.files, server: dev.server, rpc: dev.rpc });
55
56
  else {
56
57
  say(`\n${shown(dir)} on this machine: ${dev.url}`);
57
58
  say(` server: ${dev.server ? (dev.server.running ? 'running' : 'not running (above)') : 'none (no server/)'}`);
58
59
  say(` sign-in, saves, scores: a local player; kept in ${shown(dev.store.file)}`);
60
+ say(` project files (env.GF.files, xcore files): public ones at ${dev.files}/, an origin of their own`);
59
61
  say(` economy RPC: ${dev.rpc}`);
60
62
  say('Ctrl+C stops it.');
61
63
  }
@@ -70,5 +72,5 @@ export default {
70
72
  });
71
73
  await dev.close();
72
74
  return 0;
73
- }),
75
+ }, args),
74
76
  };
@@ -12,16 +12,16 @@ const usage = `Usage: xcore domain [list] [dir] [--json]
12
12
  xcore domain remove <domain> [dir] [--json]
13
13
 
14
14
  The published game on a domain of your own (play.mygame.com), besides its xgames.fun link: players see your
15
- address, and sign-in, saves, scores, rooms and the economy work there as on games.fun. Creator has 1 domain, Scale 5;
16
- Free has none. Sign-in still opens a small games.fun window, and a post on X still uses the xgames.fun link.
15
+ address, and sign-in, saves, scores, rooms and the economy work there as on xgames.fun. Creator has 1 domain, Scale 5;
16
+ Free has none. Sign-in still opens a small xgames.fun window, and a post on X still uses the xgames.fun link.
17
17
  list the domains and their state
18
18
  add <domain> ask for one: prints the DNS records to add where your domain's DNS is
19
- check <domain> look at its records now (games.fun also checks by itself); live once they are right
19
+ check <domain> look at its records now (xgames.fun also checks by itself); live once they are right
20
20
  remove <domain> stop serving the game there
21
21
  A game whose plan no longer has room for a domain keeps it, paused: it sends players to the xgames.fun link.`;
22
22
 
23
23
  const plainly = e => {
24
- // games.fun's own words already say what to do; add the command only when they don't
24
+ // xgames.fun's own words already say what to do; add the command only when they don't
25
25
  if (e?.code === 'plan')
26
26
  throw new Error(
27
27
  /plan activate/.test(e.message)
@@ -29,7 +29,7 @@ const plainly = e => {
29
29
  : `${e.message} Custom domains come with Creator (1) and Scale (5): put the game in a slot with ${CMD} plan activate.`,
30
30
  );
31
31
  if (e?.code === 'domains-off')
32
- throw new Error(`${e.message} (custom domains need games.fun's operator to set them up first.)`);
32
+ throw new Error(`${e.message} (custom domains need xgames.fun's operator to set them up first.)`);
33
33
  throw e;
34
34
  };
35
35
 
@@ -72,11 +72,11 @@ export default {
72
72
  );
73
73
  return 0;
74
74
  }
75
- if (!d) throw new Error(`games.fun did not answer with ${host}.`);
75
+ if (!d) throw new Error(`xgames.fun did not answer with ${host}.`);
76
76
  console.log(describeDomain(d));
77
77
  if (d.status !== 'active')
78
78
  console.log(
79
- `\nThen: ${CMD} domain check ${host} (DNS can take a few minutes; games.fun also checks by itself).`,
79
+ `\nThen: ${CMD} domain check ${host} (DNS can take a few minutes; xgames.fun also checks by itself).`,
80
80
  );
81
81
  // the page must know its domain: gamesfun.js 1.2.0 asks the play edge which game it is on a host of its own
82
82
  const stale = await staleClient(dir).catch(() => []);
@@ -85,5 +85,5 @@ export default {
85
85
  `\nThis folder's ${stale.join(', ')} is older than this xcore's: on its own domain an older copy can't tell which game it is, so sign-in fails there. Run ${CMD} add signin ${dir}, then ${CMD} publish ${dir}, so the page knows its domain.`,
86
86
  );
87
87
  return 0;
88
- }),
88
+ }, args),
89
89
  };
@@ -9,13 +9,13 @@ const usage = `Usage: xcore embed [list] [dir] [--json]
9
9
  xcore embed add <site> [dir] [--json]
10
10
  xcore embed remove <site> [dir] [--json]
11
11
 
12
- The sites of your own that may show the published game in a frame (an <iframe> in your page), besides games.fun
12
+ The sites of your own that may show the published game in a frame (an <iframe> in your page), besides xgames.fun
13
13
  and posts on X. A site is its origin: https://mygame.com (http://localhost:<port> while you build it). It applies
14
14
  within about a minute, with no publish. Free allows 3 sites, Creator 10, Scale 25.
15
15
  list the sites, and where the game plays
16
16
  add <site> allow one, and print the <iframe> to put in its page
17
17
  remove <site> take one off
18
- Framed on your site, the game signs players in by itself (a small games.fun window, or a code), as it does in a post
18
+ Framed on your site, the game signs players in by itself (a small xgames.fun window, or a code), as it does in a post
19
19
  on X: your page can't sign them in for it.`;
20
20
 
21
21
  export default {
@@ -53,5 +53,5 @@ export default {
53
53
  );
54
54
  } else console.log(r.changed ? `${r.origin} may no longer show the game.` : `${r.origin} was not on the list.`);
55
55
  return 0;
56
- }),
56
+ }, args),
57
57
  };