@fzgame/cli 0.0.0-stage → 0.1.0
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 +15 -0
- package/README.md +313 -2
- package/dist/cli.cjs +1077 -0
- package/dist/cli.cjs.map +1 -0
- package/example/click-counter/README.md +11 -0
- package/example/click-counter/editor/index.html +68 -0
- package/example/click-counter/fzgame.json +65 -0
- package/example/click-counter/game/index.html +75 -0
- package/example/click-counter/handler/index.js +13 -0
- package/package.json +61 -4
- package/src/cli.ts +11 -0
- package/src/dev.ts +311 -0
- package/src/env.ts +30 -0
- package/src/fetch.ts +37 -0
- package/src/fzgame.schema.json +69 -0
- package/src/init.ts +74 -0
- package/src/kit-path.ts +40 -0
- package/src/manifest.ts +182 -0
- package/src/pack.ts +108 -0
- package/src/program.ts +110 -0
- package/src/publish.ts +59 -0
- package/src/runner-pipeline.ts +48 -0
- package/src/runner-receipt.ts +35 -0
- package/src/runner.ts +304 -0
- package/src/version.ts +19 -0
- package/src/zip.ts +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fanzeal
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,314 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Develop an FZ game
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This guide covers creating a game project and running it on your machine. You do not need GameHub, MinIO, or a published build for day-to-day work. `fzgame dev` serves the game, the editor, and a mock runtime that `@fzgame/kit` talks to.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Node.js 20 or later.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g @fzgame/cli
|
|
11
|
+
fzgame --version
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The published package carries the click-counter example, so `fzgame init` works without a checkout.
|
|
15
|
+
|
|
16
|
+
### From this monorepo
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm --filter @fzgame/kit build
|
|
21
|
+
pnpm --filter @fzgame/cli build
|
|
22
|
+
node packages/fzgame-cli/dist/cli.cjs --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Below, `fzgame` means the global `fzgame` command or `node packages/fzgame-cli/dist/cli.cjs` from the repo root. `fzgame --help` lists the five commands: `init`, `build`, `publish`, `dev`, and `runner`.
|
|
26
|
+
|
|
27
|
+
## Init a project
|
|
28
|
+
|
|
29
|
+
A game root is a folder with `fzgame.json` plus three directories:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
my-game/
|
|
33
|
+
fzgame.json
|
|
34
|
+
game/ # player iframe (required: game/index.html)
|
|
35
|
+
editor/ # instance config UI (required: editor/index.html)
|
|
36
|
+
handler/ # backend artifact uploaded to S3 (required: handler/index.js)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Scaffold the example
|
|
40
|
+
|
|
41
|
+
Fastest path. Click Counter is a complete local game.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
fzgame init my-game
|
|
45
|
+
cd my-game
|
|
46
|
+
fzgame dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`init` refuses a directory that already has files; pass `--force` to write into one. An existing `.gitignore` is kept.
|
|
50
|
+
|
|
51
|
+
Then edit `my-game/fzgame.json` (`info.name`, `info.slug`, `info.version`) and the files under `game/`, `editor/`, and `handler/`.
|
|
52
|
+
|
|
53
|
+
### Create the layout yourself
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mkdir -p my-game/game my-game/editor my-game/handler
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`my-game/fzgame.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"info": {
|
|
64
|
+
"name": "My Game",
|
|
65
|
+
"slug": "my-game",
|
|
66
|
+
"version": 1,
|
|
67
|
+
"image": "",
|
|
68
|
+
"description": "",
|
|
69
|
+
"author": "",
|
|
70
|
+
"git_url": ""
|
|
71
|
+
},
|
|
72
|
+
"entry": {
|
|
73
|
+
"game": "game/index.html",
|
|
74
|
+
"editor": "editor/index.html",
|
|
75
|
+
"handler": "handler/index.js"
|
|
76
|
+
},
|
|
77
|
+
"formSchema": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {}
|
|
80
|
+
},
|
|
81
|
+
"resultSchema": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {}
|
|
84
|
+
},
|
|
85
|
+
"snapshotSchema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {}
|
|
88
|
+
},
|
|
89
|
+
"gameDataSchema": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Five parts of `fzgame.json`:
|
|
97
|
+
|
|
98
|
+
1. `info` — catalog identity: `name` (min 2 chars), `slug` (kebab-case), `version` (positive integer, default `1`), optional `image`, `description`, `author`, `git_url` (http(s))
|
|
99
|
+
2. `formSchema` — JSON Schema for the create/edit form (instance config). Optional `formUiSchema` for JSON Forms / RJSF widgets
|
|
100
|
+
3. `resultSchema` — JSON Schema for the payload `finish` / the handler returns; stored on the play session `result`
|
|
101
|
+
4. `snapshotSchema` — JSON Schema for mid-play state (`kit.setState` → `metadata.snapshot`)
|
|
102
|
+
5. `gameDataSchema` — JSON Schema for persistent instance/content data
|
|
103
|
+
|
|
104
|
+
`entry` is optional; the paths above are the defaults. Legacy flat `name` / `slug` / `version` / `configSchema` still parse (`configSchema` is an alias for `formSchema`). `kit.config()` reads `formSchema` property defaults, then instance overrides.
|
|
105
|
+
|
|
106
|
+
`my-game/game/index.html` (loads the kit, opens a play session, starts the lifecycle):
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<!doctype html>
|
|
110
|
+
<html>
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="utf-8" />
|
|
113
|
+
<title>My Game</title>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<p id="status">Loading…</p>
|
|
117
|
+
<script type="module">
|
|
118
|
+
const params = new URLSearchParams(location.search)
|
|
119
|
+
const kitUrl = params.get('kit') || './vendor/kit.js'
|
|
120
|
+
const { createKit } = await import(kitUrl)
|
|
121
|
+
|
|
122
|
+
const kit = createKit()
|
|
123
|
+
await kit.load()
|
|
124
|
+
await kit.connect()
|
|
125
|
+
await kit.emit('ready')
|
|
126
|
+
await kit.emit('start')
|
|
127
|
+
|
|
128
|
+
document.getElementById('status').textContent = kit.snapshot.status
|
|
129
|
+
</script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`my-game/editor/index.html` (saves config to the host via `postMessage`):
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<!doctype html>
|
|
138
|
+
<html>
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="utf-8" />
|
|
141
|
+
<title>My Game editor</title>
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<form id="form">
|
|
145
|
+
<button type="submit">Save</button>
|
|
146
|
+
</form>
|
|
147
|
+
<script type="module">
|
|
148
|
+
const form = document.getElementById('form')
|
|
149
|
+
window.addEventListener('message', (event) => {
|
|
150
|
+
if (event.data?.source !== 'fzgame-host' || event.data?.type !== 'init') return
|
|
151
|
+
// apply event.data.config onto the form
|
|
152
|
+
})
|
|
153
|
+
parent.postMessage({ source: 'fzgame-editor', type: 'ready' }, '*')
|
|
154
|
+
form.addEventListener('submit', (event) => {
|
|
155
|
+
event.preventDefault()
|
|
156
|
+
parent.postMessage({ source: 'fzgame-editor', type: 'save', config: {} }, '*')
|
|
157
|
+
})
|
|
158
|
+
</script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`my-game/handler/index.js`:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
export default {
|
|
167
|
+
async fetch() {
|
|
168
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
169
|
+
headers: { 'content-type': 'application/json' },
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Start local development
|
|
176
|
+
|
|
177
|
+
From the game root:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
fzgame dev
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or from anywhere:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
fzgame dev --cwd my-game
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`dev` serves the game, the editor, and a mock runtime, and needs no GameHub instance. From a monorepo checkout without a global install, `node packages/fzgame-cli/dist/cli.cjs dev --cwd my-game`.
|
|
190
|
+
|
|
191
|
+
Options:
|
|
192
|
+
|
|
193
|
+
| Flag | Default | Meaning |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `--cwd <dir>` | current directory | Game root (must contain `fzgame.json`) |
|
|
196
|
+
| `-p, --port <port>` | `3400` | Dev server port |
|
|
197
|
+
| `--host <host>` | `127.0.0.1` | Bind address. `0.0.0.0` exposes the mock runtime on the LAN; it has no authentication, so the default keeps it on this machine |
|
|
198
|
+
|
|
199
|
+
Open [http://127.0.0.1:3400](http://127.0.0.1:3400) for the launcher, or attach the portal studio while `fzgame dev` is running:
|
|
200
|
+
|
|
201
|
+
[http://localhost:3000/game-editor/sandbox](http://localhost:3000/game-editor/sandbox)
|
|
202
|
+
|
|
203
|
+
The sandbox shows play on the left and the editor on the right. The game URL includes a `config` query JSON so the kit can load instance config. Device size, a resizable event console, and kit events (`load`, `connect`, `emit`, `snapshot`) plus editor saves live on the same page. A different origin is `?origin=http://127.0.0.1:3400`.
|
|
204
|
+
|
|
205
|
+
The launcher links to:
|
|
206
|
+
|
|
207
|
+
- **Open game** — `/game/index.html` with `gameInstanceId`, `playerToken`, `apiUrl`, and `kit` already in the query string
|
|
208
|
+
- **Open editor** — `/editor/index.html` with the same `apiUrl` / `kit`
|
|
209
|
+
|
|
210
|
+
Reload the browser after you edit HTML, JS, or CSS. The server reads files from disk on each request.
|
|
211
|
+
|
|
212
|
+
### What the dev server provides
|
|
213
|
+
|
|
214
|
+
- Static files from the game root (`/game`, `/editor`, `/handler`)
|
|
215
|
+
- `@fzgame/kit` at `/@fzgame/kit.js`
|
|
216
|
+
- Mock GameHub runtime at `/games/runtime` (in-memory sessions, no database)
|
|
217
|
+
- `/health` for the portal sandbox (CORS is open so the studio can watch the origin)
|
|
218
|
+
|
|
219
|
+
The game page is launched as:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
/game/index.html?gameInstanceId=dev-instance&playerToken=dev-player&apiUrl=http://127.0.0.1:3400&kit=/@fzgame/kit.js
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`createKit()` reads `apiUrl`, `gameInstanceId`, and `playerToken` from that query string.
|
|
226
|
+
|
|
227
|
+
Sandbox and editor preview use `?preview=true&gameSetting={...}` instead. That skips play-session calls and applies the JSON as `kit.config()`.
|
|
228
|
+
|
|
229
|
+
If `/@fzgame/kit.js` returns 404, the kit is missing its build output: reinstall the CLI, or in a monorepo run `pnpm --filter @fzgame/kit build`.
|
|
230
|
+
|
|
231
|
+
## Talk to GameHub from the game
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
import { createKit } from '/@fzgame/kit.js' // in `fzgame dev`; use `./vendor/kit.js` after publish
|
|
235
|
+
|
|
236
|
+
const kit = createKit()
|
|
237
|
+
await kit.load() // GET /games/runtime/instances/:gameInstanceId
|
|
238
|
+
await kit.connect() // POST /games/runtime/sessions { gameInstanceId, playerToken }
|
|
239
|
+
await kit.emit('ready')
|
|
240
|
+
await kit.emit('start')
|
|
241
|
+
await kit.setState({ score: 1 }) // local snapshot + PUT .../snapshot
|
|
242
|
+
await kit.emit('score', { score: 1 })
|
|
243
|
+
await kit.emit('finish', { score: 1, result: { score: 1 } })
|
|
244
|
+
|
|
245
|
+
kit.config() // formSchema defaults merged with instance overrides
|
|
246
|
+
kit.snapshot // { status, score, result, state }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Lifecycle:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
CREATED → READY → PLAYING ⇄ PAUSED → FINISHED
|
|
253
|
+
↘ abort / expire
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Emit `ready`, `start`, `pause`, `finish`, `abort`, `score`, or `snapshot`. `finish` and `score` accept `{ score, result }`.
|
|
257
|
+
|
|
258
|
+
On a published GameHub instance the host still passes `apiUrl`, `gameInstanceId`, and `playerToken`. `playerToken` is the player's `identity` or id (the player must already exist).
|
|
259
|
+
|
|
260
|
+
## Editor host protocol
|
|
261
|
+
|
|
262
|
+
The portal and BO load `editor/index.html` in an iframe.
|
|
263
|
+
|
|
264
|
+
- Host → editor: `{ source: 'fzgame-host', type: 'init', config, configSchema, gameInstanceId }` (`configSchema` is the unwrapped `formSchema`)
|
|
265
|
+
- Editor → host: `{ source: 'fzgame-editor', type: 'ready' }`
|
|
266
|
+
- Editor → host: `{ source: 'fzgame-editor', type: 'save', config }`
|
|
267
|
+
|
|
268
|
+
In local `fzgame dev` there is no parent to persist config. Use the editor page to check the UI; save is wired when the same HTML is opened from the portal instance editor.
|
|
269
|
+
|
|
270
|
+
## Next: pack and publish
|
|
271
|
+
|
|
272
|
+
When the game runs locally:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
fzgame build --cwd my-game
|
|
276
|
+
fzgame publish --cwd my-game --api http://localhost:3020 --token <session>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Every flag falls back to an environment variable, and `fzgame` reads a `.env` file from the current directory before it parses arguments. Values already exported in the shell win over the file.
|
|
280
|
+
|
|
281
|
+
| Variable | Used by | Meaning |
|
|
282
|
+
| --- | --- | --- |
|
|
283
|
+
| `FZGAME_API` | `publish`, `runner start` | GameHub API (default `http://localhost:3020`) |
|
|
284
|
+
| `FZGAME_SERVER` | `runner start` | Alias for `FZGAME_API` |
|
|
285
|
+
| `FZGAME_TOKEN` | `publish` | Session token, same as `--token` |
|
|
286
|
+
| `FZGAME_SPACE_ID` | `publish` | Space id, publishes a space-private game |
|
|
287
|
+
| `FZGAME_RUNNER_KEY` | `runner start` | Runner key from the BO receipt |
|
|
288
|
+
| `FZGAME_RUNNER_PASS` | `runner start` | Runner pass from the BO receipt |
|
|
289
|
+
|
|
290
|
+
## CI runner
|
|
291
|
+
|
|
292
|
+
BO **Runners → Generate runner** opens the runner page with a one-time pass and CLI / Docker start commands. Start the worker with `--server` pointing at the GameHub API:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
fzgame runner start --server http://localhost:3020 --key fzr_... --pass ...
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
From this repo:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
node packages/fzgame-cli/dist/cli.cjs runner start --server http://localhost:3020 --key fzr_... --pass ...
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Docker (after generating the receipt):
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
docker compose run --rm runner --server http://host.docker.internal:3020 --key fzr_... --pass ...
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Or set `FZGAME_RUNNER_KEY` and `FZGAME_RUNNER_PASS` and `docker compose up -d runner`. The runner claims queued git deploys, then fetch → build → optional test → publish preview.
|
|
311
|
+
|
|
312
|
+
`build` writes `my-game/dist/<slug>-v<version>.zip`. `publish` uploads that zip to `/games/publishing` (add `--space <id>` for a space-private game). The zip is validated, then `game/`, `editor/`, and `handler/` are stored on MinIO/S3.
|
|
313
|
+
|
|
314
|
+
Publishing needs the GameHub API and a logged-in session token. It is not required for local development.
|