@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/dist/cli.cjs ADDED
@@ -0,0 +1,1077 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.1_@swc+helpers@0.5.15__jiti@2.7.0_postcss@8.5.26_tsx@4.23.13_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
5
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
6
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
7
+
8
+ // src/env.ts
9
+ var import_node_fs = require("fs");
10
+ var import_node_path = require("path");
11
+ function loadEnvFile(dir = process.cwd()) {
12
+ let raw;
13
+ try {
14
+ raw = (0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, ".env"), "utf8");
15
+ } catch {
16
+ return false;
17
+ }
18
+ for (const line of raw.split("\n")) {
19
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
20
+ if (!match) continue;
21
+ const key = match[1];
22
+ let value = match[2];
23
+ const quoted = value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'");
24
+ if (quoted) value = value.slice(1, -1);
25
+ if (process.env[key] === void 0) process.env[key] = value;
26
+ }
27
+ return true;
28
+ }
29
+
30
+ // src/program.ts
31
+ var import_commander = require("commander");
32
+
33
+ // src/dev.ts
34
+ var import_node_fs3 = require("fs");
35
+ var import_promises2 = require("fs/promises");
36
+ var import_node_http = require("http");
37
+ var import_node_path4 = require("path");
38
+
39
+ // src/kit-path.ts
40
+ var import_node_fs2 = require("fs");
41
+ var import_node_module = require("module");
42
+ var import_node_path2 = require("path");
43
+ var require2 = (0, import_node_module.createRequire)(importMetaUrl);
44
+ var KIT_HINT = "Run `pnpm --filter @fzgame/kit build` (monorepo) or `npm install` (installed CLI)";
45
+ function resolveTarget(target) {
46
+ if (typeof target === "string") return target;
47
+ if (!target) return void 0;
48
+ return resolveTarget(target.import) ?? resolveTarget(target.default);
49
+ }
50
+ function resolveKitFile() {
51
+ const manifestPath = require2.resolve("@fzgame/kit/package.json");
52
+ const manifest = JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
53
+ const entry = manifest.module ?? resolveTarget(manifest.exports?.["."]) ?? manifest.main ?? "./dist/index.js";
54
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(manifestPath), entry);
55
+ }
56
+
57
+ // src/pack.ts
58
+ var import_promises = require("fs/promises");
59
+ var import_node_path3 = require("path");
60
+
61
+ // src/manifest.ts
62
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
63
+ var DEFAULT_ENTRY = {
64
+ game: "game/index.html",
65
+ editor: "editor/index.html",
66
+ handler: "handler/index.js"
67
+ };
68
+ function toStoredConfigSchema(manifest) {
69
+ return {
70
+ formSchema: manifest.formSchema ?? { type: "object", properties: {} },
71
+ ...manifest.formUiSchema ? { formUiSchema: manifest.formUiSchema } : {},
72
+ ...manifest.resultSchema ? { resultSchema: manifest.resultSchema } : {},
73
+ ...manifest.snapshotSchema ? { snapshotSchema: manifest.snapshotSchema } : {},
74
+ ...manifest.gameDataSchema ? { gameDataSchema: manifest.gameDataSchema } : {},
75
+ ...manifest.info.author ? { author: manifest.info.author } : {},
76
+ ...manifest.info.gitUrl ? { git_url: manifest.info.gitUrl } : {},
77
+ ...manifest.info.image ? { image: manifest.info.image } : {}
78
+ };
79
+ }
80
+ function parseFzGameManifest(input) {
81
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
82
+ throw new Error("fzgame.json must be an object");
83
+ }
84
+ const raw = input;
85
+ const info = parseInfo(raw);
86
+ const formSchema = readObject(raw.formSchema, "formSchema") ?? readObject(raw.configSchema, "configSchema");
87
+ const manifest = {
88
+ info,
89
+ entry: parseEntry(raw.entry),
90
+ formSchema,
91
+ formUiSchema: readObject(raw.formUiSchema, "formUiSchema"),
92
+ resultSchema: readObject(raw.resultSchema, "resultSchema"),
93
+ snapshotSchema: readObject(raw.snapshotSchema, "snapshotSchema"),
94
+ gameDataSchema: readObject(raw.gameDataSchema, "gameDataSchema"),
95
+ name: info.name,
96
+ slug: info.slug,
97
+ version: info.version,
98
+ description: info.description
99
+ };
100
+ manifest.configSchema = toStoredConfigSchema(manifest);
101
+ return manifest;
102
+ }
103
+ function parseInfo(raw) {
104
+ const nested = raw.info && typeof raw.info === "object" && !Array.isArray(raw.info) ? raw.info : {};
105
+ const name = pickString(nested.name, raw.name);
106
+ const slug = pickString(nested.slug, raw.slug);
107
+ if (!name || name.trim().length < 2) {
108
+ throw new Error("fzgame.json must include a name (min 2 characters)");
109
+ }
110
+ if (!slug || !SLUG_RE.test(slug)) {
111
+ throw new Error(
112
+ "fzgame.json slug must be kebab-case (lowercase letters, digits, hyphens)"
113
+ );
114
+ }
115
+ const versionRaw = nested.version ?? raw.version;
116
+ const version = versionRaw === void 0 || versionRaw === null ? 1 : versionRaw;
117
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
118
+ throw new Error("fzgame.json version must be a positive integer");
119
+ }
120
+ const gitUrl = optionalString(nested.git_url ?? nested.gitUrl);
121
+ if (gitUrl && !/^https?:\/\//i.test(gitUrl)) {
122
+ throw new Error("fzgame.json info.git_url must be an http(s) URL");
123
+ }
124
+ const image = optionalString(nested.image);
125
+ const description = optionalString(nested.description ?? raw.description);
126
+ const author = optionalString(nested.author);
127
+ return {
128
+ name: name.trim(),
129
+ slug,
130
+ version,
131
+ ...image ? { image } : {},
132
+ ...description ? { description } : {},
133
+ ...author ? { author } : {},
134
+ ...gitUrl ? { gitUrl } : {}
135
+ };
136
+ }
137
+ function parseEntry(input) {
138
+ if (input === void 0 || input === null) return { ...DEFAULT_ENTRY };
139
+ if (typeof input !== "object" || Array.isArray(input)) {
140
+ throw new Error("fzgame.json entry must be an object");
141
+ }
142
+ const raw = input;
143
+ return {
144
+ game: readPath(raw.game, DEFAULT_ENTRY.game),
145
+ editor: readPath(raw.editor, DEFAULT_ENTRY.editor),
146
+ handler: readPath(raw.handler, DEFAULT_ENTRY.handler)
147
+ };
148
+ }
149
+ function readPath(value, fallback) {
150
+ if (value === void 0 || value === null) return fallback;
151
+ if (typeof value !== "string" || !value.trim()) {
152
+ throw new Error("fzgame.json entry paths must be relative");
153
+ }
154
+ const path = value.replace(/\\/g, "/").replace(/^\.\//, "");
155
+ if (path.startsWith("/") || path.includes("..")) {
156
+ throw new Error("fzgame.json entry paths must be relative without ..");
157
+ }
158
+ return path;
159
+ }
160
+ function readObject(value, field) {
161
+ if (value === void 0 || value === null) return void 0;
162
+ if (typeof value !== "object" || Array.isArray(value)) {
163
+ throw new Error(`fzgame.json ${field} must be a JSON Schema object`);
164
+ }
165
+ return value;
166
+ }
167
+ function pickString(...values) {
168
+ for (const value of values) {
169
+ if (typeof value === "string" && value.trim()) return value;
170
+ }
171
+ return void 0;
172
+ }
173
+ function optionalString(value) {
174
+ if (value === void 0 || value === null || value === "") return void 0;
175
+ return String(value);
176
+ }
177
+
178
+ // src/zip.ts
179
+ var import_node_zlib = require("zlib");
180
+ var DOS_TIME = 0;
181
+ var DOS_DATE = 33;
182
+ var CRC_TABLE = new Uint32Array(256);
183
+ for (let n = 0; n < 256; n++) {
184
+ let c = n;
185
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
186
+ CRC_TABLE[n] = c;
187
+ }
188
+ function crc32(buf) {
189
+ let c = 4294967295;
190
+ for (let i = 0; i < buf.length; i++) {
191
+ c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
192
+ }
193
+ return (c ^ 4294967295) >>> 0;
194
+ }
195
+ function buildZip(files) {
196
+ const records = [];
197
+ const parts = [];
198
+ let offset = 0;
199
+ for (const [name, content] of Object.entries(files)) {
200
+ const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
201
+ const compressed = (0, import_node_zlib.deflateRawSync)(data);
202
+ const crc = crc32(data);
203
+ const nameBuf = Buffer.from(name.replace(/\\/g, "/"), "utf8");
204
+ const local = Buffer.alloc(30);
205
+ local.writeUInt32LE(67324752, 0);
206
+ local.writeUInt16LE(20, 4);
207
+ local.writeUInt16LE(8, 8);
208
+ local.writeUInt16LE(DOS_TIME, 10);
209
+ local.writeUInt16LE(DOS_DATE, 12);
210
+ local.writeUInt32LE(crc, 14);
211
+ local.writeUInt32LE(compressed.length, 18);
212
+ local.writeUInt32LE(data.length, 22);
213
+ local.writeUInt16LE(nameBuf.length, 26);
214
+ parts.push(local, nameBuf, compressed);
215
+ records.push({ name: nameBuf, data, compressed, crc, offset });
216
+ offset += local.length + nameBuf.length + compressed.length;
217
+ }
218
+ const centralStart = offset;
219
+ for (const record of records) {
220
+ const central = Buffer.alloc(46);
221
+ central.writeUInt32LE(33639248, 0);
222
+ central.writeUInt16LE(20, 4);
223
+ central.writeUInt16LE(20, 6);
224
+ central.writeUInt16LE(8, 10);
225
+ central.writeUInt16LE(DOS_TIME, 12);
226
+ central.writeUInt16LE(DOS_DATE, 14);
227
+ central.writeUInt32LE(record.crc, 16);
228
+ central.writeUInt32LE(record.compressed.length, 20);
229
+ central.writeUInt32LE(record.data.length, 24);
230
+ central.writeUInt16LE(record.name.length, 28);
231
+ central.writeUInt32LE(record.offset, 42);
232
+ parts.push(central, record.name);
233
+ offset += central.length + record.name.length;
234
+ }
235
+ const eocd = Buffer.alloc(22);
236
+ eocd.writeUInt32LE(101010256, 0);
237
+ eocd.writeUInt16LE(records.length, 8);
238
+ eocd.writeUInt16LE(records.length, 10);
239
+ eocd.writeUInt32LE(offset - centralStart, 12);
240
+ eocd.writeUInt32LE(centralStart, 16);
241
+ parts.push(eocd);
242
+ return Buffer.concat(parts);
243
+ }
244
+
245
+ // src/pack.ts
246
+ async function loadManifest(root) {
247
+ const manifestPath = (0, import_node_path3.join)(root, "fzgame.json");
248
+ let text;
249
+ try {
250
+ text = await (0, import_promises.readFile)(manifestPath, "utf8");
251
+ } catch (error) {
252
+ if (error.code === "ENOENT") {
253
+ throw new Error(`No fzgame.json in ${root}. Run fzgame dev from a game root.`);
254
+ }
255
+ throw error;
256
+ }
257
+ try {
258
+ return parseFzGameManifest(JSON.parse(text));
259
+ } catch (error) {
260
+ throw new Error(`Invalid fzgame.json in ${root}: ${error.message}`);
261
+ }
262
+ }
263
+ async function collectGameFiles(root, options = {}) {
264
+ const manifest = await loadManifest(root);
265
+ const files = {
266
+ "fzgame.json": await (0, import_promises.readFile)((0, import_node_path3.join)(root, "fzgame.json"))
267
+ };
268
+ for (const dir of ["game", "editor", "handler"]) {
269
+ const abs = (0, import_node_path3.join)(root, dir);
270
+ if (!await exists(abs)) {
271
+ throw new Error(`Missing required directory: ${dir}/`);
272
+ }
273
+ await walk(abs, async (file) => {
274
+ const rel = (0, import_node_path3.relative)(root, file).replace(/\\/g, "/");
275
+ files[rel] = await (0, import_promises.readFile)(file);
276
+ });
277
+ }
278
+ for (const path of [manifest.entry.game, manifest.entry.editor, manifest.entry.handler]) {
279
+ if (!files[path]) throw new Error(`Missing required entrypoint: ${path}`);
280
+ }
281
+ await vendorKit(files, options);
282
+ return files;
283
+ }
284
+ async function buildGame(root, outFile, options = {}) {
285
+ const manifest = await loadManifest(root);
286
+ const files = await collectGameFiles(root, options);
287
+ const zip = buildZip(files);
288
+ const dest = outFile || (0, import_node_path3.join)(root, "dist", `${manifest.slug}-v${manifest.version}.zip`);
289
+ await (0, import_promises.mkdir)((0, import_node_path3.dirname)(dest), { recursive: true });
290
+ await (0, import_promises.writeFile)(dest, zip);
291
+ return { dest, manifest, files: Object.keys(files) };
292
+ }
293
+ async function vendorKit(files, options) {
294
+ if (files["game/vendor/kit.js"]) return;
295
+ if (options.kitFile === null) return;
296
+ let kitFile;
297
+ if (options.kitFile) {
298
+ kitFile = options.kitFile;
299
+ } else {
300
+ try {
301
+ kitFile = resolveKitFile();
302
+ } catch (error) {
303
+ throw new Error(
304
+ `Cannot vendor the game runtime: @fzgame/kit is not installed (${error.message}). ${KIT_HINT}`
305
+ );
306
+ }
307
+ }
308
+ if (!await exists(kitFile)) {
309
+ throw new Error(`Cannot vendor the game runtime: ${kitFile} does not exist. ${KIT_HINT}`);
310
+ }
311
+ files["game/vendor/kit.js"] = await (0, import_promises.readFile)(kitFile);
312
+ }
313
+ async function walk(dir, visit) {
314
+ const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
315
+ for (const entry of entries) {
316
+ if (entry.name === ".DS_Store") continue;
317
+ const abs = (0, import_node_path3.join)(dir, entry.name);
318
+ if (entry.isDirectory()) await walk(abs, visit);
319
+ else await visit(abs);
320
+ }
321
+ }
322
+ async function exists(path) {
323
+ try {
324
+ await (0, import_promises.stat)(path);
325
+ return true;
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ // src/dev.ts
332
+ var DEFAULT_HOST = "127.0.0.1";
333
+ function applyCors(res) {
334
+ res.setHeader("access-control-allow-origin", "*");
335
+ res.setHeader("access-control-allow-headers", "*");
336
+ res.setHeader("access-control-allow-methods", "GET,POST,PUT,OPTIONS");
337
+ }
338
+ var MIME = {
339
+ ".html": "text/html; charset=utf-8",
340
+ ".js": "application/javascript; charset=utf-8",
341
+ ".mjs": "application/javascript; charset=utf-8",
342
+ ".css": "text/css; charset=utf-8",
343
+ ".json": "application/json; charset=utf-8",
344
+ ".png": "image/png",
345
+ ".svg": "image/svg+xml"
346
+ };
347
+ async function startDevServer(root, port, options = {}) {
348
+ const host = options.host || DEFAULT_HOST;
349
+ const manifest = await loadManifest(root);
350
+ const sessions = /* @__PURE__ */ new Map();
351
+ const server = (0, import_node_http.createServer)(async (req, res) => {
352
+ applyCors(res);
353
+ applySecurityHeaders(res);
354
+ if (req.method === "OPTIONS") {
355
+ res.writeHead(204);
356
+ res.end();
357
+ return;
358
+ }
359
+ try {
360
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
361
+ if (url.pathname === "/health") {
362
+ res.writeHead(200, { "content-type": "application/json" });
363
+ res.end(JSON.stringify({ ok: true, name: manifest.name, port }));
364
+ return;
365
+ }
366
+ if (url.pathname.startsWith("/games/runtime") || url.pathname.startsWith("/pub/game-instances")) {
367
+ await handleRuntime(req, res, url, sessions, manifest);
368
+ return;
369
+ }
370
+ if (url.pathname === "/@fzgame/kit.js") {
371
+ await serveKit(res);
372
+ return;
373
+ }
374
+ if (url.pathname === "/" || url.pathname === "/index.html") {
375
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
376
+ res.end(launcherHtml(requestOrigin(req, host, port), manifest.name));
377
+ return;
378
+ }
379
+ await serveStatic(root, url.pathname, res);
380
+ } catch (error) {
381
+ res.writeHead(500, { "content-type": "application/json" });
382
+ res.end(JSON.stringify({ message: error.message }));
383
+ }
384
+ });
385
+ await new Promise((resolveReady, reject) => {
386
+ server.once("error", (error) => {
387
+ reject(
388
+ error.code === "EADDRINUSE" ? new Error(`Port ${port} is already in use. Pass --port to pick another one.`) : new Error(`Cannot serve on ${host}:${port} (${error.code || error.message}).`)
389
+ );
390
+ });
391
+ server.listen(port, host, () => resolveReady());
392
+ });
393
+ const address = server.address();
394
+ return {
395
+ server,
396
+ host,
397
+ port: typeof address === "object" && address ? address.port : port,
398
+ manifest
399
+ };
400
+ }
401
+ function applySecurityHeaders(res) {
402
+ res.setHeader("Access-Control-Allow-Origin", "*");
403
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
404
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
405
+ res.setHeader("Content-Security-Policy", "frame-ancestors *");
406
+ res.removeHeader("X-Frame-Options");
407
+ }
408
+ async function serveKit(res) {
409
+ try {
410
+ const body = await (0, import_promises2.readFile)(resolveKitFile());
411
+ res.writeHead(200, { "content-type": "application/javascript; charset=utf-8" });
412
+ res.end(body);
413
+ } catch (error) {
414
+ res.writeHead(404, { "content-type": "text/plain" });
415
+ res.end(`Cannot serve @fzgame/kit: ${error.message}
416
+ ${KIT_HINT}`);
417
+ }
418
+ }
419
+ async function serveStatic(root, pathname, res) {
420
+ const rel = decodeURIComponent(pathname).replace(/^\//, "") || "index.html";
421
+ const abs = (0, import_node_path4.resolve)(root, rel);
422
+ const rootAbs = (0, import_node_path4.resolve)(root);
423
+ if (abs !== rootAbs && !abs.startsWith(rootAbs + import_node_path4.sep)) {
424
+ res.writeHead(403);
425
+ res.end("Forbidden");
426
+ return;
427
+ }
428
+ try {
429
+ const info = await (0, import_promises2.stat)(abs);
430
+ const file = info.isDirectory() ? (0, import_node_path4.join)(abs, "index.html") : abs;
431
+ res.writeHead(200, {
432
+ "content-type": MIME[(0, import_node_path4.extname)(file)] || "application/octet-stream"
433
+ });
434
+ (0, import_node_fs3.createReadStream)(file).pipe(res);
435
+ } catch {
436
+ res.writeHead(404);
437
+ res.end("Not found");
438
+ }
439
+ }
440
+ async function handleRuntime(req, res, url, sessions, manifest) {
441
+ res.setHeader("content-type", "application/json");
442
+ res.setHeader("access-control-allow-origin", "*");
443
+ res.setHeader("access-control-allow-headers", "content-type");
444
+ res.setHeader("access-control-allow-methods", "GET,POST,PUT,OPTIONS");
445
+ if (req.method === "OPTIONS") {
446
+ res.writeHead(204);
447
+ res.end();
448
+ return;
449
+ }
450
+ if (req.method === "GET" && (url.pathname.startsWith("/pub/game-instances/") || url.pathname.startsWith("/games/runtime/instances/"))) {
451
+ res.end(
452
+ JSON.stringify({
453
+ id: url.pathname.split("/").pop(),
454
+ name: manifest.name,
455
+ status: "PUBLISHED",
456
+ overrides: {},
457
+ game: { name: manifest.name, configSchema: manifest.configSchema ?? {} }
458
+ })
459
+ );
460
+ return;
461
+ }
462
+ if (req.method === "POST" && url.pathname === "/games/runtime/sessions") {
463
+ const body = await readJson(req);
464
+ const existing = [...sessions.values()].find(
465
+ (s) => s.gameInstanceId === body.gameInstanceId && s.status !== "FINISHED"
466
+ );
467
+ if (existing) {
468
+ res.end(JSON.stringify(existing));
469
+ return;
470
+ }
471
+ const session = {
472
+ id: `dev-${sessions.size + 1}`,
473
+ token: `dev-token-${sessions.size + 1}`,
474
+ status: "CREATED",
475
+ score: null,
476
+ result: null,
477
+ metadata: { snapshot: {} },
478
+ gameInstanceId: String(body.gameInstanceId || "dev-instance")
479
+ };
480
+ sessions.set(session.token, session);
481
+ res.end(JSON.stringify(session));
482
+ return;
483
+ }
484
+ const emitMatch = url.pathname.match(/^\/games\/runtime\/sessions\/([^/]+)\/emit$/);
485
+ if (req.method === "POST" && emitMatch) {
486
+ const session = sessions.get(emitMatch[1]);
487
+ if (!session) {
488
+ res.writeHead(404);
489
+ res.end(JSON.stringify({ message: "Session not found" }));
490
+ return;
491
+ }
492
+ const body = await readJson(req);
493
+ applyDevEmit(
494
+ session,
495
+ String(body.type || ""),
496
+ body.payload
497
+ );
498
+ res.end(JSON.stringify(session));
499
+ return;
500
+ }
501
+ const snapMatch = url.pathname.match(/^\/games\/runtime\/sessions\/([^/]+)\/snapshot$/);
502
+ if (req.method === "PUT" && snapMatch) {
503
+ const session = sessions.get(snapMatch[1]);
504
+ if (!session) {
505
+ res.writeHead(404);
506
+ res.end(JSON.stringify({ message: "Session not found" }));
507
+ return;
508
+ }
509
+ const body = await readJson(req);
510
+ session.metadata.snapshot = body.state ?? body;
511
+ res.end(JSON.stringify(session));
512
+ return;
513
+ }
514
+ const getMatch = url.pathname.match(/^\/games\/runtime\/sessions\/([^/]+)$/);
515
+ if (req.method === "GET" && getMatch) {
516
+ const session = sessions.get(getMatch[1]);
517
+ if (!session) {
518
+ res.writeHead(404);
519
+ res.end(JSON.stringify({ message: "Session not found" }));
520
+ return;
521
+ }
522
+ res.end(JSON.stringify(session));
523
+ return;
524
+ }
525
+ res.writeHead(404);
526
+ res.end(JSON.stringify({ message: "Not found" }));
527
+ }
528
+ function applyDevEmit(session, type, payload) {
529
+ if (type === "ready") session.status = "READY";
530
+ if (type === "start") session.status = "PLAYING";
531
+ if (type === "pause") session.status = "PAUSED";
532
+ if (type === "finish") {
533
+ session.status = "FINISHED";
534
+ if (typeof payload?.score === "number") session.score = payload.score;
535
+ if (payload?.result) session.result = payload.result;
536
+ }
537
+ if (type === "abort") session.status = "ABORTED";
538
+ if (type === "score" && typeof payload?.score === "number") session.score = payload.score;
539
+ if (type === "snapshot") session.metadata.snapshot = payload?.state ?? payload;
540
+ }
541
+ function readJson(req) {
542
+ return new Promise((resolveJson, reject) => {
543
+ const chunks = [];
544
+ req.on("data", (c) => chunks.push(c));
545
+ req.on("end", () => {
546
+ if (chunks.length === 0) return resolveJson({});
547
+ try {
548
+ resolveJson(JSON.parse(Buffer.concat(chunks).toString("utf8")));
549
+ } catch (error) {
550
+ reject(error);
551
+ }
552
+ });
553
+ req.on("error", reject);
554
+ });
555
+ }
556
+ function requestOrigin(req, host, port) {
557
+ if (req.headers.host) return `http://${req.headers.host}`;
558
+ const shown = host === "0.0.0.0" || host === "::" ? DEFAULT_HOST : host;
559
+ return `http://${shown}:${port}`;
560
+ }
561
+ function launcherHtml(origin, name) {
562
+ const kit = `kit=${encodeURIComponent("/@fzgame/kit.js")}`;
563
+ const play = `/game/index.html?gameInstanceId=dev-instance&playerToken=dev-player&apiUrl=${encodeURIComponent(origin)}&${kit}`;
564
+ const preview = `/game/index.html?preview=true&${kit}`;
565
+ const editor = `/editor/index.html?gameInstanceId=dev-instance&apiUrl=${encodeURIComponent(origin)}&${kit}`;
566
+ return `<!doctype html>
567
+ <html>
568
+ <head><meta charset="utf-8"><title>${name} \u2014 fzgame dev</title></head>
569
+ <body style="font-family:system-ui;padding:24px">
570
+ <h1>${name}</h1>
571
+ <p>Local game developer mode.</p>
572
+ <p><a href="${play}">Open game</a></p>
573
+ <p><a href="${preview}">Open preview</a> (no play session; pass <code>gameSetting</code> JSON to try config)</p>
574
+ <p><a href="${editor}">Open editor</a></p>
575
+ </body>
576
+ </html>`;
577
+ }
578
+
579
+ // src/init.ts
580
+ var import_promises3 = require("fs/promises");
581
+ var import_node_path5 = require("path");
582
+ var import_node_url = require("url");
583
+ var here = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
584
+ var EXAMPLE_DIR = (0, import_node_path5.join)(here, "..", "example", "click-counter");
585
+ async function initGame(targetDir, options = {}) {
586
+ const dest = (0, import_node_path5.resolve)(targetDir);
587
+ const template = await (0, import_promises3.stat)(EXAMPLE_DIR).then(
588
+ (info) => info.isDirectory(),
589
+ () => false
590
+ );
591
+ if (!template) {
592
+ throw new Error(
593
+ `Game template missing at ${EXAMPLE_DIR}. Reinstall @fzgame/cli, the "example" folder must ship with it.`
594
+ );
595
+ }
596
+ await (0, import_promises3.mkdir)(dest, { recursive: true });
597
+ const existing = (await (0, import_promises3.readdir)(dest)).filter((name) => name !== ".DS_Store" && name !== ".git");
598
+ if (existing.length > 0 && !options.force) {
599
+ throw new Error(
600
+ `${dest} is not empty (${existing.slice(0, 5).join(", ")}${existing.length > 5 ? ", ..." : ""}). Pass --force to write into it.`
601
+ );
602
+ }
603
+ const hasGitignore = await (0, import_promises3.stat)((0, import_node_path5.join)(dest, ".gitignore")).then(
604
+ () => true,
605
+ () => false
606
+ );
607
+ await (0, import_promises3.cp)(EXAMPLE_DIR, dest, {
608
+ recursive: true,
609
+ filter: (source) => keep(source) && !(hasGitignore && source.endsWith(`${import_node_path5.sep}.gitignore`))
610
+ });
611
+ await ensureGitignore(dest);
612
+ const files = [];
613
+ await walk2(dest, (file) => {
614
+ files.push((0, import_node_path5.relative)(dest, file).split(import_node_path5.sep).join("/"));
615
+ });
616
+ files.sort();
617
+ return { dir: dest, files };
618
+ }
619
+ function keep(source) {
620
+ const name = source.split(import_node_path5.sep).pop() ?? "";
621
+ return name !== "dist" && name !== "node_modules" && name !== ".DS_Store";
622
+ }
623
+ async function ensureGitignore(dest) {
624
+ const path = (0, import_node_path5.join)(dest, ".gitignore");
625
+ let current = "";
626
+ try {
627
+ current = await (0, import_promises3.readFile)(path, "utf8");
628
+ } catch {
629
+ }
630
+ if (/(^|\n)\s*dist\/?\s*(\n|$)/.test(current)) return;
631
+ const next = current && !current.endsWith("\n") ? `${current}
632
+ ` : current;
633
+ await (0, import_promises3.writeFile)(path, `${next}dist/
634
+ `);
635
+ }
636
+ async function walk2(dir, visit) {
637
+ for (const entry of await (0, import_promises3.readdir)(dir, { withFileTypes: true })) {
638
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
639
+ const abs = (0, import_node_path5.join)(dir, entry.name);
640
+ if (entry.isDirectory()) await walk2(abs, visit);
641
+ else visit(abs);
642
+ }
643
+ }
644
+
645
+ // src/publish.ts
646
+ var import_promises4 = require("fs/promises");
647
+
648
+ // src/fetch.ts
649
+ var DEFAULT_TIMEOUT_MS = 3e4;
650
+ async function request(url, options = {}) {
651
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, ...init } = options;
652
+ try {
653
+ return await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
654
+ } catch (error) {
655
+ throw new Error(`${url} ${describeFailure(error, timeoutMs)}`);
656
+ }
657
+ }
658
+ function describeFailure(error, timeoutMs) {
659
+ const name = error?.name;
660
+ if (name === "TimeoutError" || name === "AbortError") {
661
+ return `did not respond within ${Math.round(timeoutMs / 1e3)}s`;
662
+ }
663
+ const cause = error?.cause;
664
+ const detail = cause instanceof AggregateError ? cause.errors.map(codeOf).join(", ") : codeOf(cause) ?? error?.message ?? String(error);
665
+ return `failed: ${detail}`;
666
+ }
667
+ function codeOf(error) {
668
+ if (error instanceof Error) return error.code ?? error.message;
669
+ return String(error);
670
+ }
671
+
672
+ // src/publish.ts
673
+ var UPLOAD_TIMEOUT_MS = 12e4;
674
+ async function publishGame(options) {
675
+ const { dest, manifest } = await buildGame(options.cwd);
676
+ const zip = await (0, import_promises4.readFile)(dest);
677
+ const api2 = options.api.replace(/\/$/, "");
678
+ const path = options.spaceId ? "/portal/games/publishing" : "/games/publishing";
679
+ const url = `${api2}${path}`;
680
+ const form = new FormData();
681
+ form.append(
682
+ "file",
683
+ new Blob([new Uint8Array(zip)], { type: "application/zip" }),
684
+ `${manifest.slug}.zip`
685
+ );
686
+ if (options.spaceId) form.set("spaceId", options.spaceId);
687
+ const res = await request(url, {
688
+ method: "POST",
689
+ headers: {
690
+ authorization: `Bearer ${options.token}`,
691
+ cookie: `better-auth.session_token=${options.token}`,
692
+ ...options.spaceId ? { "x-space-id": options.spaceId } : {}
693
+ },
694
+ body: form,
695
+ timeoutMs: UPLOAD_TIMEOUT_MS
696
+ });
697
+ const text = await res.text();
698
+ if (!res.ok) {
699
+ throw new Error(`Publish failed (${res.status}): ${text.slice(0, 500)}`);
700
+ }
701
+ const contentType = res.headers.get("content-type") || "";
702
+ if (!contentType.includes("json")) {
703
+ throw new Error(
704
+ `Publish returned ${res.status} ${contentType || "without a content type"} instead of JSON: ${text.slice(0, 200)}`
705
+ );
706
+ }
707
+ try {
708
+ return JSON.parse(text);
709
+ } catch {
710
+ throw new Error(`Publish returned invalid JSON: ${text.slice(0, 200)}`);
711
+ }
712
+ }
713
+
714
+ // src/runner.ts
715
+ var import_node_child_process = require("child_process");
716
+ var import_promises5 = require("fs/promises");
717
+ var import_node_os = require("os");
718
+ var import_node_os2 = require("os");
719
+ var import_node_path7 = require("path");
720
+
721
+ // src/runner-pipeline.ts
722
+ function planPipeline(scripts) {
723
+ const stages = ["fetch", "build"];
724
+ if (scripts?.test) stages.push("test");
725
+ stages.push("publish");
726
+ return stages;
727
+ }
728
+ function installCommand(files) {
729
+ if (!files.includes("package.json")) return null;
730
+ if (files.includes("pnpm-lock.yaml")) {
731
+ return ["pnpm", ["install", "--frozen-lockfile"]];
732
+ }
733
+ if (files.includes("yarn.lock")) {
734
+ return ["yarn", ["install", "--frozen-lockfile"]];
735
+ }
736
+ if (files.includes("package-lock.json")) return ["npm", ["ci"]];
737
+ return ["npm", ["install"]];
738
+ }
739
+ function buildCommand(files, scripts) {
740
+ if (!scripts?.build) return null;
741
+ const [manager] = installCommand(files.includes("package.json") ? files : ["package.json", ...files]) ?? [
742
+ "npm",
743
+ []
744
+ ];
745
+ return [manager, ["run", "build"]];
746
+ }
747
+ function testCommand(files, scripts) {
748
+ if (!scripts?.test) return null;
749
+ const [manager] = installCommand(files.includes("package.json") ? files : ["package.json", ...files]) ?? [
750
+ "npm",
751
+ []
752
+ ];
753
+ return [manager, ["run", "test"]];
754
+ }
755
+
756
+ // src/version.ts
757
+ var import_node_fs4 = require("fs");
758
+ var import_node_path6 = require("path");
759
+ var import_node_url2 = require("url");
760
+ var here2 = (0, import_node_path6.dirname)((0, import_node_url2.fileURLToPath)(importMetaUrl));
761
+ function readVersion() {
762
+ try {
763
+ const raw = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path6.join)(here2, "..", "package.json"), "utf8"));
764
+ return raw.version ?? "0.0.0";
765
+ } catch {
766
+ return "0.0.0";
767
+ }
768
+ }
769
+ var VERSION = readVersion();
770
+
771
+ // src/runner.ts
772
+ var EXAMPLE = "fzgame runner start --server http://localhost:3020 --key <key> --pass <pass>";
773
+ var CLAIM_TIMEOUT_MS = 3e4;
774
+ var IDLE_SLEEP_MS = 2e3;
775
+ var MIN_BACKOFF_MS = 2e3;
776
+ var MAX_BACKOFF_MS = 3e4;
777
+ var UPLOAD_TIMEOUT_MS2 = 12e4;
778
+ var LOG_FLUSH_MS = 250;
779
+ var LOG_MAX_CHARS = 8e3;
780
+ async function startRunner(opts) {
781
+ const server = (opts.server || "").replace(/\/$/, "");
782
+ const key = opts.key || "";
783
+ const pass = opts.pass || "";
784
+ if (!server || !key || !pass) {
785
+ throw new Error(`Pass --server, --key, and --pass. Example: ${EXAMPLE}`);
786
+ }
787
+ const headers = {
788
+ "x-runner-key": key,
789
+ "x-runner-pass": pass
790
+ };
791
+ const connected = await api(server, "/internal/ci-runners/connect", {
792
+ method: "POST",
793
+ headers: { ...headers, "Content-Type": "application/json" },
794
+ body: JSON.stringify({
795
+ key,
796
+ pass,
797
+ hostname: (0, import_node_os.hostname)(),
798
+ version: VERSION
799
+ })
800
+ });
801
+ console.log(`Runner ${connected.name || key} connected to ${server}`);
802
+ console.log("Waiting for queued git deploys (fetch \u2192 build \u2192 test \u2192 publish)");
803
+ let stopping = false;
804
+ const beat = setInterval(() => {
805
+ api(server, "/internal/ci-runners/heartbeat", {
806
+ method: "POST",
807
+ headers
808
+ }).catch(() => void 0);
809
+ }, 15e3);
810
+ const stop = () => {
811
+ if (stopping) return;
812
+ stopping = true;
813
+ clearInterval(beat);
814
+ api(server, "/internal/ci-runners/disconnect", {
815
+ method: "POST",
816
+ headers
817
+ }).catch(() => void 0).finally(() => process.exit(0));
818
+ };
819
+ process.on("SIGINT", stop);
820
+ process.on("SIGTERM", stop);
821
+ let backoff = MIN_BACKOFF_MS;
822
+ const retry = async (message) => {
823
+ console.error(`${message}; retrying in ${Math.round(backoff / 1e3)}s`);
824
+ await sleep(backoff);
825
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
826
+ };
827
+ while (!stopping) {
828
+ let res;
829
+ try {
830
+ res = await request(`${server}/internal/ci-runners/jobs/next`, {
831
+ headers,
832
+ timeoutMs: CLAIM_TIMEOUT_MS
833
+ });
834
+ } catch (error) {
835
+ await retry(`claim failed: ${error.message}`);
836
+ continue;
837
+ }
838
+ backoff = MIN_BACKOFF_MS;
839
+ if (res.status === 204) {
840
+ await sleep(IDLE_SLEEP_MS);
841
+ continue;
842
+ }
843
+ const text = await res.text();
844
+ if (!res.ok) {
845
+ await retry(`claim failed (${res.status}): ${text.slice(0, 200)}`);
846
+ continue;
847
+ }
848
+ if (!text) {
849
+ await sleep(IDLE_SLEEP_MS);
850
+ continue;
851
+ }
852
+ let job;
853
+ try {
854
+ job = JSON.parse(text);
855
+ } catch {
856
+ await retry(`claim returned invalid JSON: ${text.slice(0, 200)}`);
857
+ continue;
858
+ }
859
+ console.log(`Job ${job.deploymentId} ${job.owner}/${job.repo}@${job.commitSha.slice(0, 7)}`);
860
+ try {
861
+ await runJob(server, headers, job);
862
+ console.log(`Job ${job.deploymentId} ready`);
863
+ } catch (err) {
864
+ const message = err.message;
865
+ console.error(`Job ${job.deploymentId} failed: ${message}`);
866
+ await api(server, `/internal/ci-runners/jobs/${job.deploymentId}/fail`, {
867
+ method: "POST",
868
+ headers: { ...headers, "Content-Type": "application/json" },
869
+ body: JSON.stringify({ message })
870
+ }).catch(() => void 0);
871
+ }
872
+ }
873
+ }
874
+ async function runJob(server, headers, job) {
875
+ const work = await (0, import_promises5.mkdtemp)((0, import_node_path7.join)((0, import_node_os2.tmpdir)(), "fzgame-run-"));
876
+ let pending = "";
877
+ let flushTimer;
878
+ const flushLogs = async () => {
879
+ if (flushTimer) {
880
+ clearTimeout(flushTimer);
881
+ flushTimer = void 0;
882
+ }
883
+ if (!pending) return;
884
+ const chunk = pending;
885
+ pending = "";
886
+ await api(server, `/internal/ci-runners/jobs/${job.deploymentId}/logs`, {
887
+ method: "POST",
888
+ headers: { ...headers, "Content-Type": "application/json" },
889
+ body: JSON.stringify({ chunk })
890
+ }).catch(() => void 0);
891
+ };
892
+ const log = (chunk) => {
893
+ process.stdout.write(chunk);
894
+ pending += chunk;
895
+ if (pending.length >= LOG_MAX_CHARS) {
896
+ void flushLogs();
897
+ return;
898
+ }
899
+ if (!flushTimer) {
900
+ flushTimer = setTimeout(() => void flushLogs(), LOG_FLUSH_MS);
901
+ flushTimer.unref();
902
+ }
903
+ };
904
+ const stage = async (name, status, error) => {
905
+ await flushLogs();
906
+ await api(server, `/internal/ci-runners/jobs/${job.deploymentId}/stage`, {
907
+ method: "POST",
908
+ headers: { ...headers, "Content-Type": "application/json" },
909
+ body: JSON.stringify({ name, status, error })
910
+ });
911
+ };
912
+ try {
913
+ await stage("fetch", "running");
914
+ log(`fetch ${job.owner}/${job.repo} ${job.commitSha}
915
+ `);
916
+ await runCmd("git", ["init"], work, log);
917
+ const origin = `https://x-access-token:${job.cloneToken}@github.com/${job.owner}/${job.repo}.git`;
918
+ await runCmd("git", ["remote", "add", "origin", origin], work, () => void 0);
919
+ await runCmd("git", ["fetch", "--depth", "1", "origin", job.commitSha], work, log);
920
+ await runCmd("git", ["checkout", "--force", "FETCH_HEAD"], work, log);
921
+ await stage("fetch", "ok");
922
+ const packRoot = job.rootDirectory ? (0, import_node_path7.join)(work, job.rootDirectory) : work;
923
+ const files = await (0, import_promises5.readdir)(packRoot);
924
+ const scripts = await readScripts(packRoot);
925
+ const planned = planPipeline(scripts);
926
+ await stage("build", "running");
927
+ const install = installCommand(files);
928
+ if (install) await runCmd(install[0], install[1], packRoot, log);
929
+ const build = buildCommand(files, scripts);
930
+ if (build) await runCmd(build[0], build[1], packRoot, log);
931
+ await stage("build", "ok");
932
+ if (planned.includes("test")) {
933
+ await stage("test", "running");
934
+ const test = testCommand(files, scripts);
935
+ if (test) await runCmd(test[0], test[1], packRoot, log);
936
+ await stage("test", "ok");
937
+ } else {
938
+ await stage("test", "skipped");
939
+ }
940
+ await stage("publish", "running");
941
+ const packed = await buildGame(packRoot);
942
+ log(`packed ${packed.dest}
943
+ `);
944
+ const zip = await (0, import_promises5.readFile)(packed.dest);
945
+ const form = new FormData();
946
+ form.append(
947
+ "file",
948
+ new Blob([new Uint8Array(zip)], { type: "application/zip" }),
949
+ "game.zip"
950
+ );
951
+ const published = await request(
952
+ `${server}/internal/ci-runners/jobs/${job.deploymentId}/artifact`,
953
+ { method: "POST", headers, body: form, timeoutMs: UPLOAD_TIMEOUT_MS2 }
954
+ );
955
+ if (!published.ok) {
956
+ throw new Error(`publish failed (${published.status}): ${await published.text()}`);
957
+ }
958
+ await stage("publish", "ok");
959
+ } finally {
960
+ await flushLogs();
961
+ await (0, import_promises5.rm)(work, { recursive: true, force: true });
962
+ }
963
+ }
964
+ async function readScripts(root) {
965
+ try {
966
+ const raw = JSON.parse(await (0, import_promises5.readFile)((0, import_node_path7.join)(root, "package.json"), "utf8"));
967
+ return raw.scripts ?? {};
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+ function runCmd(cmd, args, cwd, onLog) {
973
+ return new Promise((resolve3, reject) => {
974
+ const useShell = process.platform === "win32";
975
+ const child = (0, import_node_child_process.spawn)(cmd, useShell ? args.map(quoteForShell) : args, {
976
+ cwd,
977
+ env: process.env,
978
+ shell: useShell
979
+ });
980
+ child.stdout.on("data", (buf) => onLog(buf.toString()));
981
+ child.stderr.on("data", (buf) => onLog(buf.toString()));
982
+ child.on("error", reject);
983
+ child.on("close", (code, signal) => {
984
+ if (code === 0) resolve3();
985
+ else if (signal) reject(new Error(`${cmd} ${args.join(" ")} killed by ${signal}`));
986
+ else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
987
+ });
988
+ });
989
+ }
990
+ function quoteForShell(arg) {
991
+ return /[\s"&|<>^()]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
992
+ }
993
+ async function api(server, path, init) {
994
+ const res = await request(`${server}${path}`, init);
995
+ const text = await res.text();
996
+ if (!res.ok) throw new Error(`${path} failed (${res.status}): ${text.slice(0, 500)}`);
997
+ if (!text) return {};
998
+ try {
999
+ return JSON.parse(text);
1000
+ } catch {
1001
+ throw new Error(`${path} returned invalid JSON: ${text.slice(0, 200)}`);
1002
+ }
1003
+ }
1004
+ function sleep(ms) {
1005
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1006
+ }
1007
+
1008
+ // src/program.ts
1009
+ var DEFAULT_API = "http://localhost:3020";
1010
+ function buildProgram() {
1011
+ const program = new import_commander.Command();
1012
+ program.name("fzgame").description("Build, publish, and develop FZ games").version(VERSION).addHelpText(
1013
+ "after",
1014
+ `
1015
+ Environment (a .env file in the working directory is read too, shell values win):
1016
+ FZGAME_API GameHub API for publish and runner (default ${DEFAULT_API})
1017
+ FZGAME_SERVER Alias for FZGAME_API
1018
+ FZGAME_TOKEN Session token for publish
1019
+ FZGAME_SPACE_ID Space id, publishes a private game
1020
+ FZGAME_RUNNER_KEY Runner key from the BO receipt
1021
+ FZGAME_RUNNER_PASS Runner pass from the BO receipt`
1022
+ );
1023
+ program.command("init [dir]").description("Scaffold a game from the bundled click-counter example").option("-f, --force", "write into a directory that already has files").action(async (dir, opts) => {
1024
+ const result = await initGame(dir || ".", { force: opts.force });
1025
+ console.log(`Scaffolded ${result.files.length} files in ${result.dir}`);
1026
+ for (const file of result.files) console.log(` ${file}`);
1027
+ console.log("Next: fzgame dev");
1028
+ });
1029
+ program.command("build").description("Validate fzgame.json and pack game/editor/handler into a zip").option("--cwd <dir>", "game root", process.cwd()).option("-o, --out <file>", "output zip path").action(async (opts) => {
1030
+ const result = await buildGame(opts.cwd, opts.out);
1031
+ console.log(
1032
+ `Built ${result.manifest.slug}@${result.manifest.version} -> ${result.dest}`
1033
+ );
1034
+ });
1035
+ program.command("publish").description("Build and upload the zip to /games/publishing").option("--cwd <dir>", "game root", process.cwd()).option("--api <url>", "GameHub API", process.env.FZGAME_API || DEFAULT_API).option("--token <token>", "session token", process.env.FZGAME_TOKEN).option("--space <id>", "space id for private games", process.env.FZGAME_SPACE_ID).action(async (opts) => {
1036
+ if (!opts.token) {
1037
+ throw new Error("Pass --token or set FZGAME_TOKEN");
1038
+ }
1039
+ const game = await publishGame({
1040
+ cwd: opts.cwd,
1041
+ api: opts.api,
1042
+ token: opts.token,
1043
+ spaceId: opts.space
1044
+ });
1045
+ console.log(`Published ${game.slug}@${game.version} (${game.id})`);
1046
+ if (game.entryPointUrl) console.log(game.entryPointUrl);
1047
+ });
1048
+ program.command("dev").description("Serve the game locally with a mock runtime kit API").option("--cwd <dir>", "game root", process.cwd()).option("-p, --port <port>", "port", "3400").option("--host <host>", "bind address (use 0.0.0.0 to expose on the LAN)", "127.0.0.1").action(async (opts) => {
1049
+ const { port, manifest } = await startDevServer(opts.cwd, Number(opts.port), {
1050
+ host: opts.host
1051
+ });
1052
+ const shown = opts.host === "0.0.0.0" || opts.host === "::" ? "127.0.0.1" : opts.host;
1053
+ console.log(`${manifest.name} dev server http://${shown}:${port}`);
1054
+ if (shown !== opts.host) console.log(`Bound to ${opts.host}, reachable from the LAN`);
1055
+ });
1056
+ const runner = program.command("runner").description("Self-hosted GameHub CI runner");
1057
+ runner.command("start").description("Connect to the API and run fetch \u2192 build \u2192 test \u2192 publish jobs").option(
1058
+ "--server <url>",
1059
+ "GameHub API",
1060
+ process.env.FZGAME_API || process.env.FZGAME_SERVER || DEFAULT_API
1061
+ ).option("--key <key>", "runner key from the BO receipt", process.env.FZGAME_RUNNER_KEY).option("--pass <pass>", "runner pass from the BO receipt", process.env.FZGAME_RUNNER_PASS).action(async (opts) => {
1062
+ await startRunner({
1063
+ server: opts.server,
1064
+ key: opts.key || "",
1065
+ pass: opts.pass || ""
1066
+ });
1067
+ });
1068
+ return program;
1069
+ }
1070
+
1071
+ // src/cli.ts
1072
+ loadEnvFile();
1073
+ buildProgram().parseAsync(process.argv).catch((error) => {
1074
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
1075
+ process.exit(1);
1076
+ });
1077
+ //# sourceMappingURL=cli.cjs.map