@boardgamebuddy/game-pack-cli 0.0.1 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +84 -0
  2. package/cli.js +271 -271
  3. package/package.json +24 -13
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @boardgamebuddy/game-pack-cli
2
+
3
+ Developer CLI for creating and testing [BoardGameBuddy](https://github.com/BoardGameBuddy) game packs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @boardgamebuddy/game-pack-cli
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `bgb new <game-id>`
14
+
15
+ Scaffold a new game pack from the upstream template.
16
+
17
+ ```bash
18
+ bgb new ticket-to-ride
19
+ bgb new ticket-to-ride --name "Ticket to Ride"
20
+ ```
21
+
22
+ **Arguments**
23
+
24
+ | Argument | Description |
25
+ |---|---|
26
+ | `game-id` | Unique identifier for the game. Lowercase alphanumeric and hyphens only (e.g. `ticket-to-ride`). |
27
+
28
+ **Options**
29
+
30
+ | Option | Description |
31
+ |---|---|
32
+ | `-n, --name <displayName>` | Display name shown in the app. Defaults to the title-cased `game-id`. |
33
+
34
+ This command downloads the template from [`BoardGameBuddy/game-packs`](https://github.com/BoardGameBuddy/game-packs) and creates the pack under `<game-id>/` in the current directory:
35
+
36
+ ```
37
+ ticket-to-ride/
38
+ game.json ← patched with your game-id and display name
39
+ scorer.ts ← implement your scoring logic here
40
+ scorer.js ← compiled output (generated)
41
+ embeddings.json ← card embeddings
42
+ ```
43
+
44
+ ### `bgb serve [pack-dir]`
45
+
46
+ Start a local dev server for a game pack with live reload whenever `scorer.ts` changes.
47
+
48
+ ```bash
49
+ # from inside the pack directory
50
+ cd games/ticket-to-ride
51
+ bgb serve
52
+
53
+ # or pass the path explicitly
54
+ bgb serve games/ticket-to-ride
55
+ ```
56
+
57
+ The server starts on port `3000` (override with the `PORT` environment variable) and prints a QR code you can scan in the BoardGameBuddy app to load the pack directly.
58
+
59
+ When `scorer.ts` changes, the CLI automatically recompiles it with `tsc` and pushes a reload event to the app via Server-Sent Events.
60
+
61
+ ## Developing a game pack
62
+
63
+ 1. **Scaffold** the pack:
64
+ ```bash
65
+ bgb new my-game
66
+ ```
67
+
68
+ 2. **Implement scoring** in `my-game/scorer.ts`. The `score` function receives all players and their detected cards and must return a score result for each player:
69
+ ```ts
70
+ export function score(players: PlayerInput[]): PlayerScoreResult[] {
71
+ // your logic here
72
+ }
73
+ ```
74
+
75
+ 3. **Start the dev server**:
76
+ ```bash
77
+ bgb serve my-game
78
+ ```
79
+
80
+ 4. **Open the app**, navigate to Pack Store, and scan the QR code to load your pack. The app reloads automatically whenever you save `scorer.ts`.
81
+
82
+ ## Requirements
83
+
84
+ - Node.js >= 18
package/cli.js CHANGED
@@ -1,271 +1,271 @@
1
- #!/usr/bin/env node
2
- /**
3
- * bgb — BoardGameBuddy game pack developer CLI
4
- *
5
- * Commands:
6
- * bgb new <game-id> [--name "Display Name"] scaffold a new pack from the upstream template
7
- * bgb serve [pack-dir] serve a pack with live reload on scorer.ts changes
8
- */
9
-
10
- 'use strict';
11
-
12
- const { program } = require('commander');
13
- const https = require('https');
14
- const http = require('http');
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
- const { spawn } = require('child_process');
19
-
20
- const PORT = parseInt(process.env.PORT || '3000', 10);
21
-
22
- const TEMPLATE_REPO = 'BoardGameBuddy/game-packs';
23
- const TEMPLATE_PATH = 'games/_template';
24
- const TEMPLATE_REF = 'main';
25
-
26
- // ─── Helpers ────────────────────────────────────────────────────────────────
27
-
28
- function getLocalIp() {
29
- for (const ifaces of Object.values(os.networkInterfaces())) {
30
- for (const iface of ifaces) {
31
- if (iface.family === 'IPv4' && !iface.internal) return iface.address;
32
- }
33
- }
34
- return 'localhost';
35
- }
36
-
37
- function toTitleCase(str) {
38
- return str
39
- .split('-')
40
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
41
- .join(' ');
42
- }
43
-
44
- function get(url) {
45
- return new Promise((resolve, reject) => {
46
- const lib = url.startsWith('https') ? https : http;
47
- lib.get(url, { headers: { 'User-Agent': 'bgb-cli' } }, (res) => {
48
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
49
- resolve(get(res.headers.location));
50
- return;
51
- }
52
- const chunks = [];
53
- res.on('data', chunk => chunks.push(chunk));
54
- res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
55
- }).on('error', reject);
56
- });
57
- }
58
-
59
- async function githubContents(repoPath) {
60
- const url = `https://api.github.com/repos/${TEMPLATE_REPO}/contents/${repoPath}?ref=${TEMPLATE_REF}`;
61
- const { status, body } = await get(url);
62
- if (status !== 200) {
63
- throw new Error(`GitHub API returned ${status} for ${repoPath}:\n${body.toString()}`);
64
- }
65
- return JSON.parse(body.toString());
66
- }
67
-
68
- async function downloadTemplate(repoPath, localDir) {
69
- const entries = await githubContents(repoPath);
70
- fs.mkdirSync(localDir, { recursive: true });
71
- for (const entry of entries) {
72
- const dest = path.join(localDir, entry.name);
73
- if (entry.type === 'dir') {
74
- await downloadTemplate(entry.path, dest);
75
- } else if (entry.type === 'file') {
76
- const { status, body } = await get(entry.download_url);
77
- if (status !== 200) throw new Error(`Failed to download ${entry.path}: HTTP ${status}`);
78
- fs.writeFileSync(dest, body);
79
- }
80
- }
81
- }
82
-
83
- // ─── bgb new ────────────────────────────────────────────────────────────────
84
-
85
- program
86
- .command('new <game-id>')
87
- .description('Scaffold a new game pack from the upstream template')
88
- .option('-n, --name <displayName>', 'Display name for the game (defaults to title-cased game-id)')
89
- .action(async (gameId, opts) => {
90
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(gameId)) {
91
- console.error(`Error: game-id must be lowercase alphanumeric and hyphens only (got: "${gameId}")`);
92
- process.exit(1);
93
- }
94
-
95
- const displayName = opts.name || toTitleCase(gameId);
96
- const targetDir = path.join(process.cwd(), 'games', gameId);
97
-
98
- if (fs.existsSync(targetDir)) {
99
- console.error(`Error: directory already exists: ${targetDir}`);
100
- process.exit(1);
101
- }
102
-
103
- console.log(`Fetching template from ${TEMPLATE_REPO}...`);
104
- try {
105
- await downloadTemplate(TEMPLATE_PATH, targetDir);
106
- } catch (err) {
107
- console.error(`Error: failed to fetch template — ${err.message}`);
108
- process.exit(1);
109
- }
110
-
111
- // Patch game.json
112
- const gameJsonPath = path.join(targetDir, 'game.json');
113
- let gameJson = fs.readFileSync(gameJsonPath, 'utf8');
114
- gameJson = gameJson.replace(/"mygame"/g, `"${gameId}"`);
115
- gameJson = gameJson.replace(/"My Game"/g, `"${displayName}"`);
116
- fs.writeFileSync(gameJsonPath, gameJson);
117
-
118
- // Patch embeddings.json
119
- const embeddingsPath = path.join(targetDir, 'embeddings.json');
120
- if (fs.existsSync(embeddingsPath)) {
121
- let embeddings = fs.readFileSync(embeddingsPath, 'utf8');
122
- embeddings = embeddings.replace(/"mygame:/g, `"${gameId}:`);
123
- fs.writeFileSync(embeddingsPath, embeddings);
124
- }
125
-
126
- console.log(`\nCreated game pack: games/${gameId}/`);
127
- console.log(` Display name: ${displayName}`);
128
- console.log(`\nNext steps:`);
129
- console.log(` cd games/${gameId}`);
130
- console.log(` # Edit scorer.ts to implement your scoring logic`);
131
- console.log(` bgb serve . # Start dev server with live reload`);
132
- console.log('');
133
- });
134
-
135
- // ─── bgb serve ──────────────────────────────────────────────────────────────
136
-
137
- program
138
- .command('serve [pack-dir]')
139
- .description('Serve a game pack with live reload on scorer.ts changes')
140
- .action((packDirArg) => {
141
- const packDir = packDirArg ? path.resolve(packDirArg) : process.cwd();
142
- const gameJsonPath = path.join(packDir, 'game.json');
143
-
144
- if (!fs.existsSync(gameJsonPath)) {
145
- console.error(`Error: no game.json found in ${packDir}`);
146
- console.error('Run this command from a pack directory or pass the pack path as argument.');
147
- process.exit(1);
148
- }
149
-
150
- const packName = path.basename(packDir);
151
-
152
- const MIME = {
153
- '.json': 'application/json',
154
- '.js': 'application/javascript',
155
- '.html': 'text/html',
156
- '.txt': 'text/plain',
157
- };
158
-
159
- const sseClients = new Set();
160
-
161
- function broadcast(data) {
162
- const msg = `data: ${JSON.stringify(data)}\n\n`;
163
- for (const res of sseClients) res.write(msg);
164
- }
165
-
166
- const server = http.createServer((req, res) => {
167
- res.setHeader('Access-Control-Allow-Origin', '*');
168
- res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
169
-
170
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
171
-
172
- const urlPath = req.url.split('?')[0];
173
-
174
- if (urlPath === '/events') {
175
- res.writeHead(200, {
176
- 'Content-Type': 'text/event-stream',
177
- 'Cache-Control': 'no-cache',
178
- 'Connection': 'keep-alive',
179
- 'Access-Control-Allow-Origin': '*',
180
- });
181
- res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
182
- sseClients.add(res);
183
-
184
- const heartbeat = setInterval(() => res.write(': ping\n\n'), 30000);
185
- req.on('close', () => { clearInterval(heartbeat); sseClients.delete(res); });
186
- return;
187
- }
188
-
189
- const filePath = path.join(packDir, urlPath === '/' ? '' : urlPath);
190
-
191
- if (!filePath.startsWith(packDir)) {
192
- res.writeHead(403); res.end('Forbidden'); return;
193
- }
194
-
195
- console.log(`${req.method} ${urlPath}`);
196
-
197
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
198
- const ext = path.extname(filePath).toLowerCase();
199
- res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
200
- fs.createReadStream(filePath).pipe(res);
201
- } else {
202
- res.writeHead(404); res.end('Not found');
203
- }
204
- });
205
-
206
- server.listen(PORT, '0.0.0.0', () => {
207
- const ip = getLocalIp();
208
- const url = `http://${ip}:${PORT}/`;
209
-
210
- console.log(`\nServing pack: ${packName}`);
211
- console.log(`URL: ${url}\n`);
212
-
213
- try {
214
- const qrcode = require('qrcode-terminal');
215
- qrcode.generate(url, { small: true }, (qr) => {
216
- console.log(qr);
217
- console.log('In the app: Pack Store → QR-Code scannen → fertig.\n');
218
- console.log('Watching scorer.ts for changes... Press Ctrl+C to stop.\n');
219
- });
220
- } catch {
221
- console.log(`In the app: Pack Store → Von URL importieren → ${url}\n`);
222
- console.log('Watching scorer.ts for changes... Press Ctrl+C to stop.\n');
223
- }
224
- });
225
-
226
- const scorerTs = path.join(packDir, 'scorer.ts');
227
- if (!fs.existsSync(scorerTs)) {
228
- console.warn('Warning: scorer.ts not found — live reload disabled.');
229
- return;
230
- }
231
-
232
- let debounceTimer = null;
233
-
234
- function recompile() {
235
- console.log('scorer.ts changed — recompiling...');
236
- const proc = spawn(
237
- 'npx',
238
- ['tsc', 'scorer.ts', '--outDir', '.', '--target', 'ES2017', '--module', 'commonjs', '--skipLibCheck'],
239
- { cwd: packDir, shell: true }
240
- );
241
-
242
- let stderr = '';
243
- proc.stderr.on('data', chunk => { stderr += chunk.toString(); });
244
- proc.stdout.on('data', chunk => { process.stdout.write(chunk); });
245
-
246
- proc.on('close', (code) => {
247
- if (code === 0) {
248
- console.log('Recompile OK — broadcasting reload.');
249
- broadcast({ type: 'reload' });
250
- } else {
251
- const msg = stderr.trim() || 'Compilation failed (see above)';
252
- console.error(`Recompile failed:\n${msg}`);
253
- broadcast({ type: 'error', message: msg });
254
- }
255
- });
256
- }
257
-
258
- fs.watch(scorerTs, { persistent: true }, () => {
259
- clearTimeout(debounceTimer);
260
- debounceTimer = setTimeout(recompile, 300);
261
- });
262
- });
263
-
264
- // ─── Parse ───────────────────────────────────────────────────────────────────
265
-
266
- program
267
- .name('bgb')
268
- .description('BoardGameBuddy game pack developer CLI')
269
- .version('1.0.0');
270
-
271
- program.parse(process.argv);
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bgb — BoardGameBuddy game pack developer CLI
4
+ *
5
+ * Commands:
6
+ * bgb new <game-id> [--name "Display Name"] scaffold a new pack from the upstream template
7
+ * bgb serve [pack-dir] serve a pack with live reload on scorer.ts changes
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const { program } = require('commander');
13
+ const https = require('https');
14
+ const http = require('http');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const { spawn } = require('child_process');
19
+
20
+ const PORT = parseInt(process.env.PORT || '3000', 10);
21
+
22
+ const TEMPLATE_REPO = 'BoardGameBuddy/game-packs';
23
+ const TEMPLATE_PATH = 'games/_template';
24
+ const TEMPLATE_REF = 'main';
25
+
26
+ // ─── Helpers ────────────────────────────────────────────────────────────────
27
+
28
+ function getLocalIp() {
29
+ for (const ifaces of Object.values(os.networkInterfaces())) {
30
+ for (const iface of ifaces) {
31
+ if (iface.family === 'IPv4' && !iface.internal) return iface.address;
32
+ }
33
+ }
34
+ return 'localhost';
35
+ }
36
+
37
+ function toTitleCase(str) {
38
+ return str
39
+ .split('-')
40
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
41
+ .join(' ');
42
+ }
43
+
44
+ function get(url) {
45
+ return new Promise((resolve, reject) => {
46
+ const lib = url.startsWith('https') ? https : http;
47
+ lib.get(url, { headers: { 'User-Agent': 'bgb-cli' } }, (res) => {
48
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
49
+ resolve(get(res.headers.location));
50
+ return;
51
+ }
52
+ const chunks = [];
53
+ res.on('data', chunk => chunks.push(chunk));
54
+ res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
55
+ }).on('error', reject);
56
+ });
57
+ }
58
+
59
+ async function githubContents(repoPath) {
60
+ const url = `https://api.github.com/repos/${TEMPLATE_REPO}/contents/${repoPath}?ref=${TEMPLATE_REF}`;
61
+ const { status, body } = await get(url);
62
+ if (status !== 200) {
63
+ throw new Error(`GitHub API returned ${status} for ${repoPath}:\n${body.toString()}`);
64
+ }
65
+ return JSON.parse(body.toString());
66
+ }
67
+
68
+ async function downloadTemplate(repoPath, localDir) {
69
+ const entries = await githubContents(repoPath);
70
+ fs.mkdirSync(localDir, { recursive: true });
71
+ for (const entry of entries) {
72
+ const dest = path.join(localDir, entry.name);
73
+ if (entry.type === 'dir') {
74
+ await downloadTemplate(entry.path, dest);
75
+ } else if (entry.type === 'file') {
76
+ const { status, body } = await get(entry.download_url);
77
+ if (status !== 200) throw new Error(`Failed to download ${entry.path}: HTTP ${status}`);
78
+ fs.writeFileSync(dest, body);
79
+ }
80
+ }
81
+ }
82
+
83
+ // ─── bgb new ────────────────────────────────────────────────────────────────
84
+
85
+ program
86
+ .command('new <game-id>')
87
+ .description('Scaffold a new game pack from the upstream template')
88
+ .option('-n, --name <displayName>', 'Display name for the game (defaults to title-cased game-id)')
89
+ .action(async (gameId, opts) => {
90
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(gameId)) {
91
+ console.error(`Error: game-id must be lowercase alphanumeric and hyphens only (got: "${gameId}")`);
92
+ process.exit(1);
93
+ }
94
+
95
+ const displayName = opts.name || toTitleCase(gameId);
96
+ const targetDir = path.join(process.cwd(), gameId);
97
+
98
+ if (fs.existsSync(targetDir)) {
99
+ console.error(`Error: directory already exists: ${targetDir}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ console.log(`Fetching template from ${TEMPLATE_REPO}...`);
104
+ try {
105
+ await downloadTemplate(TEMPLATE_PATH, targetDir);
106
+ } catch (err) {
107
+ console.error(`Error: failed to fetch template — ${err.message}`);
108
+ process.exit(1);
109
+ }
110
+
111
+ // Patch game.json
112
+ const gameJsonPath = path.join(targetDir, 'game.json');
113
+ let gameJson = fs.readFileSync(gameJsonPath, 'utf8');
114
+ gameJson = gameJson.replace(/"mygame"/g, `"${gameId}"`);
115
+ gameJson = gameJson.replace(/"My Game"/g, `"${displayName}"`);
116
+ fs.writeFileSync(gameJsonPath, gameJson);
117
+
118
+ // Patch embeddings.json
119
+ const embeddingsPath = path.join(targetDir, 'embeddings.json');
120
+ if (fs.existsSync(embeddingsPath)) {
121
+ let embeddings = fs.readFileSync(embeddingsPath, 'utf8');
122
+ embeddings = embeddings.replace(/"mygame:/g, `"${gameId}:`);
123
+ fs.writeFileSync(embeddingsPath, embeddings);
124
+ }
125
+
126
+ console.log(`\nCreated game pack: ${gameId}/`);
127
+ console.log(` Display name: ${displayName}`);
128
+ console.log(`\nNext steps:`);
129
+ console.log(` cd ${gameId}`);
130
+ console.log(` # Edit scorer.ts to implement your scoring logic`);
131
+ console.log(` bgb serve . # Start dev server with live reload`);
132
+ console.log('');
133
+ });
134
+
135
+ // ─── bgb serve ──────────────────────────────────────────────────────────────
136
+
137
+ program
138
+ .command('serve [pack-dir]')
139
+ .description('Serve a game pack with live reload on scorer.ts changes')
140
+ .action((packDirArg) => {
141
+ const packDir = packDirArg ? path.resolve(packDirArg) : process.cwd();
142
+ const gameJsonPath = path.join(packDir, 'game.json');
143
+
144
+ if (!fs.existsSync(gameJsonPath)) {
145
+ console.error(`Error: no game.json found in ${packDir}`);
146
+ console.error('Run this command from a pack directory or pass the pack path as argument.');
147
+ process.exit(1);
148
+ }
149
+
150
+ const packName = path.basename(packDir);
151
+
152
+ const MIME = {
153
+ '.json': 'application/json',
154
+ '.js': 'application/javascript',
155
+ '.html': 'text/html',
156
+ '.txt': 'text/plain',
157
+ };
158
+
159
+ const sseClients = new Set();
160
+
161
+ function broadcast(data) {
162
+ const msg = `data: ${JSON.stringify(data)}\n\n`;
163
+ for (const res of sseClients) res.write(msg);
164
+ }
165
+
166
+ const server = http.createServer((req, res) => {
167
+ res.setHeader('Access-Control-Allow-Origin', '*');
168
+ res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
169
+
170
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
171
+
172
+ const urlPath = req.url.split('?')[0];
173
+
174
+ if (urlPath === '/events') {
175
+ res.writeHead(200, {
176
+ 'Content-Type': 'text/event-stream',
177
+ 'Cache-Control': 'no-cache',
178
+ 'Connection': 'keep-alive',
179
+ 'Access-Control-Allow-Origin': '*',
180
+ });
181
+ res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
182
+ sseClients.add(res);
183
+
184
+ const heartbeat = setInterval(() => res.write(': ping\n\n'), 30000);
185
+ req.on('close', () => { clearInterval(heartbeat); sseClients.delete(res); });
186
+ return;
187
+ }
188
+
189
+ const filePath = path.join(packDir, urlPath === '/' ? '' : urlPath);
190
+
191
+ if (!filePath.startsWith(packDir)) {
192
+ res.writeHead(403); res.end('Forbidden'); return;
193
+ }
194
+
195
+ console.log(`${req.method} ${urlPath}`);
196
+
197
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
198
+ const ext = path.extname(filePath).toLowerCase();
199
+ res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
200
+ fs.createReadStream(filePath).pipe(res);
201
+ } else {
202
+ res.writeHead(404); res.end('Not found');
203
+ }
204
+ });
205
+
206
+ server.listen(PORT, '0.0.0.0', () => {
207
+ const ip = getLocalIp();
208
+ const url = `http://${ip}:${PORT}/`;
209
+
210
+ console.log(`\nServing pack: ${packName}`);
211
+ console.log(`URL: ${url}\n`);
212
+
213
+ try {
214
+ const qrcode = require('qrcode-terminal');
215
+ qrcode.generate(url, { small: true }, (qr) => {
216
+ console.log(qr);
217
+ console.log('In the app: Pack Store → QR-Code scannen → fertig.\n');
218
+ console.log('Watching scorer.ts for changes... Press Ctrl+C to stop.\n');
219
+ });
220
+ } catch {
221
+ console.log(`In the app: Pack Store → Von URL importieren → ${url}\n`);
222
+ console.log('Watching scorer.ts for changes... Press Ctrl+C to stop.\n');
223
+ }
224
+ });
225
+
226
+ const scorerTs = path.join(packDir, 'scorer.ts');
227
+ if (!fs.existsSync(scorerTs)) {
228
+ console.warn('Warning: scorer.ts not found — live reload disabled.');
229
+ return;
230
+ }
231
+
232
+ let debounceTimer = null;
233
+
234
+ function recompile() {
235
+ console.log('scorer.ts changed — recompiling...');
236
+ const proc = spawn(
237
+ 'npx',
238
+ ['tsc', 'scorer.ts', '--outDir', '.', '--target', 'ES2017', '--module', 'commonjs', '--skipLibCheck'],
239
+ { cwd: packDir, shell: true }
240
+ );
241
+
242
+ let stderr = '';
243
+ proc.stderr.on('data', chunk => { stderr += chunk.toString(); });
244
+ proc.stdout.on('data', chunk => { process.stdout.write(chunk); });
245
+
246
+ proc.on('close', (code) => {
247
+ if (code === 0) {
248
+ console.log('Recompile OK — broadcasting reload.');
249
+ broadcast({ type: 'reload' });
250
+ } else {
251
+ const msg = stderr.trim() || 'Compilation failed (see above)';
252
+ console.error(`Recompile failed:\n${msg}`);
253
+ broadcast({ type: 'error', message: msg });
254
+ }
255
+ });
256
+ }
257
+
258
+ fs.watch(scorerTs, { persistent: true }, () => {
259
+ clearTimeout(debounceTimer);
260
+ debounceTimer = setTimeout(recompile, 300);
261
+ });
262
+ });
263
+
264
+ // ─── Parse ───────────────────────────────────────────────────────────────────
265
+
266
+ program
267
+ .name('bgb')
268
+ .description('BoardGameBuddy game pack developer CLI')
269
+ .version('1.0.0');
270
+
271
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,13 +1,24 @@
1
- {
2
- "name": "@boardgamebuddy/game-pack-cli",
3
- "version": "0.0.1",
4
- "description": "Developer CLI for BoardGameBuddy game packs",
5
- "bin": { "bgb": "./cli.js" },
6
- "files": ["cli.js"],
7
- "engines": { "node": ">=18" },
8
- "publishConfig": { "access": "public" },
9
- "dependencies": {
10
- "commander": "^12.0.0",
11
- "qrcode-terminal": "^0.12.0"
12
- }
13
- }
1
+ {
2
+ "name": "@boardgamebuddy/game-pack-cli",
3
+ "version": "0.0.8",
4
+ "description": "Developer CLI for BoardGameBuddy game packs",
5
+ "bin": {
6
+ "bgb": "cli.js"
7
+ },
8
+ "repository": {
9
+ "url": "git+https://github.com/BoardGameBuddy/game-pack-cli.git"
10
+ },
11
+ "files": [
12
+ "cli.js"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "commander": "^12.0.0",
22
+ "qrcode-terminal": "^0.12.0"
23
+ }
24
+ }