@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.
- package/LICENSE +1 -1
- package/README.md +183 -84
- package/bin/xcore.mjs +13 -8
- package/commands/add.mjs +11 -8
- package/commands/adopt.mjs +9 -7
- package/commands/backend.mjs +2 -2
- package/commands/check.mjs +4 -4
- package/commands/db.mjs +218 -0
- package/commands/dev.mjs +8 -6
- package/commands/domain.mjs +8 -8
- package/commands/embed.mjs +3 -3
- package/commands/files.mjs +106 -0
- package/commands/login.mjs +9 -17
- package/commands/logout.mjs +15 -4
- package/commands/loot.mjs +2 -2
- package/commands/plan.mjs +22 -19
- package/commands/publish.mjs +54 -21
- package/commands/secret.mjs +6 -6
- package/docs/SERVER.md +218 -18
- package/lib/add.mjs +265 -40
- package/lib/browser.mjs +11 -2
- package/lib/check.mjs +10 -9
- package/lib/cli.mjs +24 -9
- package/lib/client/gamesfun.js +242 -122
- package/lib/client/godot/gamesfun.gd +17 -11
- package/lib/client/godot/gamesfun_peer.gd +2 -2
- package/lib/client/server/0001_init.sql +10 -0
- package/lib/client/server/database.js +271 -109
- package/lib/client/server/gf-db.js +55 -0
- package/lib/client/server/gf-docs.js +56 -0
- package/lib/client/server/rooms.js +16 -9
- package/lib/client/server/rpc.js +7 -7
- package/lib/client/server/stock.js +37 -0
- package/lib/client/server/vouchers.js +127 -46
- package/lib/client/unity/GamesFun.cs +15 -12
- package/lib/client/unity/GamesFun.jslib +1 -1
- package/lib/config-dir.mjs +1 -1
- package/lib/credentials.mjs +10 -5
- package/lib/db.mjs +355 -0
- package/lib/dev/d1.mjs +486 -0
- package/lib/dev/files.mjs +247 -0
- package/lib/dev/gf-local.js +69 -3
- package/lib/dev/index.mjs +68 -20
- package/lib/dev/limits.mjs +2 -2
- package/lib/dev/platform.mjs +111 -12
- package/lib/dev/server.mjs +54 -5
- package/lib/dev/tools.mjs +71 -18
- package/lib/files.mjs +208 -0
- package/lib/open.mjs +1 -1
- package/lib/play-policy.mjs +14 -5
- package/lib/publish/adopt.mjs +73 -33
- package/lib/publish/client.mjs +130 -20
- package/lib/publish/gate.mjs +4 -2
- package/lib/publish/hosting.mjs +5 -5
- package/lib/publish/index.mjs +162 -40
- package/lib/publish/oidc.mjs +26 -5
- package/lib/publish/page.mjs +1 -1
- package/lib/publish/plans.mjs +19 -12
- package/lib/publish/scan.mjs +78 -102
- package/lib/publish/secrets.mjs +4 -5
- package/lib/publish/site.mjs +9 -10
- package/lib/publish/tree.mjs +8 -8
- package/lib/secret-patterns.mjs +53 -16
- package/lib/secret-store.mjs +2 -2
- package/lib/server/bundle.mjs +113 -13
- package/lib/server/contract.mjs +440 -16
- package/lib/server/data-calls.mjs +136 -0
- package/lib/server/secrets.mjs +9 -7
- package/lib/solana-rpc.mjs +5 -5
- package/lib/state-folder.mjs +23 -0
- package/package.json +4 -4
- 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):
|
|
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 =
|
|
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.
|
|
76
|
-
|
|
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]', '', '
|
|
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
|
|
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]`:
|
|
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
|
|
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
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|
package/commands/adopt.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// `xcore adopt [folder]`: bring a web game made anywhere, to check it, add an economy and publish it on
|
|
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
|
|
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
|
|
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)
|
|
53
|
-
|
|
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
|
};
|
package/commands/backend.mjs
CHANGED
|
@@ -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
|
|
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
|
};
|
package/commands/check.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `xcore check [dir]`: the page check (../lib/check.mjs) on a game you brought: it builds, fits
|
|
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
|
|
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
|
|
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
|
};
|
package/commands/db.mjs
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
};
|
package/commands/domain.mjs
CHANGED
|
@@ -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
|
|
16
|
-
Free has none. Sign-in still opens a small
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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(`
|
|
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;
|
|
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
|
};
|
package/commands/embed.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
};
|