@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
|
@@ -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
|
+
};
|
package/commands/login.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
};
|
package/commands/logout.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16
|
-
$10 of use a month per slot, shared by your Creator games
|
|
17
|
-
Scale granted by
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
? '
|
|
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
|
};
|
package/commands/publish.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `xcore publish`: a game to
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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 = `
|
|
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
|
|
157
|
-
|
|
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')}
|
|
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
|
-
|
|
175
|
-
`
|
|
176
|
-
|
|
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;
|
|
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
|
|
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}`);
|
package/commands/secret.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `xcore secret set|list|delete`: a game server's own secrets on
|
|
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>.
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|