campus-stats 0.4.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +163 -0
  4. package/dist/cache.d.ts +10 -0
  5. package/dist/cache.js +69 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +537 -0
  8. package/dist/client.d.ts +88 -0
  9. package/dist/client.js +273 -0
  10. package/dist/data-bundle.d.ts +11 -0
  11. package/dist/data-bundle.js +17 -0
  12. package/dist/db/migrate.d.ts +6 -0
  13. package/dist/db/migrate.js +22 -0
  14. package/dist/db/sqlite-store.d.ts +12 -0
  15. package/dist/db/sqlite-store.js +344 -0
  16. package/dist/db/store-api.d.ts +9 -0
  17. package/dist/db/store-api.js +70 -0
  18. package/dist/fantasy.d.ts +36 -0
  19. package/dist/fantasy.js +152 -0
  20. package/dist/http.d.ts +25 -0
  21. package/dist/http.js +63 -0
  22. package/dist/identity/match.d.ts +22 -0
  23. package/dist/identity/match.js +106 -0
  24. package/dist/identity/propose.d.ts +8 -0
  25. package/dist/identity/propose.js +45 -0
  26. package/dist/identity/rules.d.ts +8 -0
  27. package/dist/identity/rules.js +17 -0
  28. package/dist/identity/store.d.ts +5 -0
  29. package/dist/identity/store.js +20 -0
  30. package/dist/identity/types.d.ts +32 -0
  31. package/dist/identity/types.js +8 -0
  32. package/dist/ids.d.ts +2 -0
  33. package/dist/ids.js +4 -0
  34. package/dist/index.d.ts +16 -0
  35. package/dist/index.js +9 -0
  36. package/dist/injuries.d.ts +12 -0
  37. package/dist/injuries.js +15 -0
  38. package/dist/refs.d.ts +7 -0
  39. package/dist/refs.js +1 -0
  40. package/dist/scoring.d.ts +37 -0
  41. package/dist/scoring.js +56 -0
  42. package/dist/sources/fbref-map.d.ts +14 -0
  43. package/dist/sources/fbref-map.js +56 -0
  44. package/dist/sources/fbref-parse.d.ts +17 -0
  45. package/dist/sources/fbref-parse.js +87 -0
  46. package/dist/sources/fbref.d.ts +27 -0
  47. package/dist/sources/fbref.js +72 -0
  48. package/dist/sources/index.d.ts +3 -0
  49. package/dist/sources/index.js +2 -0
  50. package/dist/sources/statsbomb-player-stats.d.ts +63 -0
  51. package/dist/sources/statsbomb-player-stats.js +107 -0
  52. package/dist/sources/statsbomb.d.ts +50 -0
  53. package/dist/sources/statsbomb.js +170 -0
  54. package/dist/sources/types.d.ts +23 -0
  55. package/dist/sources/types.js +1 -0
  56. package/dist/types.d.ts +114 -0
  57. package/dist/types.js +1 -0
  58. package/package.json +57 -0
package/dist/cli.js ADDED
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env node
2
+ import { cachePath, emptyCache, mergeSyncResult } from "./cache.js";
3
+ import { dataBundleUrl, GITLAB_PROJECT_ID, } from "./data-bundle.js";
4
+ import { readStore, resolveDbPath, resolveStoreMode, runMigrate, writeCacheToStore, writeSyncToStore, } from "./db/store-api.js";
5
+ import { FANTASY_PLAYER_STATS_LIMIT, listWomenCompetitions, syncFantasyBundle, updateCachedCompetitions, } from "./fantasy.js";
6
+ import { CampusClient } from "./client.js";
7
+ import { INJURIES_STATUS_MESSAGE } from "./injuries.js";
8
+ import { proposeIdentitiesForCompetition } from "./identity/propose.js";
9
+ import { setIdentityStatus } from "./identity/store.js";
10
+ import { FbrefSource } from "./sources/fbref.js";
11
+ import { StatsBombSource } from "./sources/statsbomb.js";
12
+ let storeMode = "json";
13
+ let dbPath = ".campus/campus.sqlite";
14
+ function usage() {
15
+ console.error(`campus — women's football data CLI
16
+
17
+ Usage:
18
+ campus sync --fantasy|--all [--with-players] [--player-stats-limit <n>]
19
+ campus sync --competition <name> [--source statsbomb|fbref] [--with-players] [--player-stats-limit <n>]
20
+ campus update [--with-players] [--player-stats-limit <n>]
21
+ campus pull [--url <cache.json url>]
22
+ campus available
23
+ campus competitions
24
+ campus seasons --competition <name>
25
+ campus teams --competition <name>
26
+ campus matches --competition <name> [--season <name>] [--team <name>]
27
+ campus players [--team <name>] [--name <name>]
28
+ campus player-stats [--competition <name>] [--match <id>] [--player <name>] [--team <name>]
29
+ campus lineups [--match <id>] [--team <name>]
30
+ campus squad --competition <name> --team <name> [--season <name>]
31
+ campus fantasy-points [--competition <name>] [--player <name>] [--match <id>]
32
+ campus injuries
33
+ campus identities [--status resolved|pending|rejected]
34
+ campus identities propose --competition <name>
35
+ campus identities confirm --id <identityId>
36
+ campus identities reject --id <identityId>
37
+ campus migrate [--db <path>]
38
+
39
+ Options:
40
+ --fantasy / --all Sync all StatsBomb women's comps (clubs + national teams)
41
+ --competition <name> Competition display name (e.g. "Liga F")
42
+ --source <id> Data source for sync (default: statsbomb)
43
+ --with-players StatsBomb only: also sync v1 player match stats
44
+ --player-stats-limit <n> Max matches to enrich (default 5; fantasy default 25)
45
+ --url <url> Override data-bundle URL for pull
46
+ --season <name> Season label (e.g. "2023/2024")
47
+ --team <name> Team name substring (case-insensitive)
48
+ --player <name> Player name substring
49
+ --name <name> Player name substring (players command)
50
+ --match <id> Match id filter
51
+ --status <status> Filter identities list
52
+ --id <identityId> Identity id for confirm/reject
53
+ --sqlite Use SQLite store (default path .campus/campus.sqlite)
54
+ --db <path> SQLite database path (implies --sqlite)
55
+ --json Force JSON cache (default)
56
+ --help Show this help
57
+
58
+ Periodic refresh: see docs/cron.md (GitLab CI schedule + campus pull).
59
+ `);
60
+ process.exit(1);
61
+ }
62
+ function getFlag(args, name) {
63
+ const idx = args.indexOf(name);
64
+ if (idx === -1)
65
+ return undefined;
66
+ const value = args[idx + 1];
67
+ if (!value || value.startsWith("--")) {
68
+ throw new Error(`Missing value for ${name}`);
69
+ }
70
+ return value;
71
+ }
72
+ function hasFlag(args, name) {
73
+ return args.includes(name);
74
+ }
75
+ function includesCI(haystack, needle) {
76
+ return haystack.toLowerCase().includes(needle.toLowerCase());
77
+ }
78
+ function requireCompetition(cache, name) {
79
+ const competition = cache.competitions.find((c) => includesCI(c.name, name) || c.name.toLowerCase() === name.toLowerCase());
80
+ const exact = cache.competitions.find((c) => c.name.toLowerCase() === name.toLowerCase());
81
+ const chosen = exact ?? competition;
82
+ if (!chosen) {
83
+ throw new Error(`Competition not in cache: "${name}". Run sync --competition first.`);
84
+ }
85
+ return chosen;
86
+ }
87
+ async function cmdSync(args) {
88
+ const fantasy = hasFlag(args, "--fantasy") || hasFlag(args, "--all");
89
+ const competition = getFlag(args, "--competition");
90
+ const withPlayers = hasFlag(args, "--with-players");
91
+ const limitRaw = getFlag(args, "--player-stats-limit");
92
+ const playerStatsLimit = limitRaw
93
+ ? Number(limitRaw)
94
+ : fantasy
95
+ ? FANTASY_PLAYER_STATS_LIMIT
96
+ : 5;
97
+ if (fantasy) {
98
+ if (competition) {
99
+ throw new Error("Use either --fantasy/--all or --competition, not both.");
100
+ }
101
+ console.error("Syncing fantasy bundle (all StatsBomb women's competitions)…");
102
+ const result = await syncFantasyBundle({
103
+ includePlayerStats: withPlayers,
104
+ playerStatsLimit,
105
+ latestSeasonPlayersOnly: true,
106
+ onProgress: (msg) => console.error(msg),
107
+ });
108
+ const cache = await writeSyncToStore(storeMode, dbPath, result);
109
+ printSyncSummary("statsbomb", result, cache);
110
+ return;
111
+ }
112
+ if (!competition)
113
+ usage();
114
+ const sourceId = (getFlag(args, "--source") ?? "statsbomb").toLowerCase();
115
+ let source;
116
+ if (sourceId === "statsbomb") {
117
+ source = new StatsBombSource({
118
+ includePlayerStats: withPlayers,
119
+ playerStatsLimit,
120
+ });
121
+ }
122
+ else if (sourceId === "fbref") {
123
+ if (withPlayers) {
124
+ throw new Error("FBref player stats are not available yet (see docs/fbref-player-stats-deferred.md).");
125
+ }
126
+ source = new FbrefSource();
127
+ }
128
+ else {
129
+ throw new Error(`Unknown source: "${sourceId}". Use statsbomb or fbref.`);
130
+ }
131
+ console.error(`Syncing "${competition}" from ${source.id}…`);
132
+ const result = await source.syncCompetition(competition);
133
+ const cache = await writeSyncToStore(storeMode, dbPath, result);
134
+ printSyncSummary(source.id, result, cache);
135
+ }
136
+ function printSyncSummary(sourceId, result, cache) {
137
+ console.log(JSON.stringify({
138
+ source: sourceId,
139
+ cache: storeMode === "json" ? cachePath() : dbPath,
140
+ competitions: result.competitions.map((c) => c.name),
141
+ seasons: result.seasons.length,
142
+ teams: result.teams.length,
143
+ matches: result.matches.length,
144
+ players: result.players?.length ?? 0,
145
+ playerMatchStats: result.playerMatchStats?.length ?? 0,
146
+ totals: {
147
+ competitions: cache.competitions.length,
148
+ seasons: cache.seasons.length,
149
+ teams: cache.teams.length,
150
+ matches: cache.matches.length,
151
+ players: cache.players.length,
152
+ playerMatchStats: cache.playerMatchStats.length,
153
+ },
154
+ }, null, 2));
155
+ }
156
+ async function cmdUpdate(args) {
157
+ const withPlayers = hasFlag(args, "--with-players");
158
+ const limitRaw = getFlag(args, "--player-stats-limit");
159
+ const playerStatsLimit = limitRaw
160
+ ? Number(limitRaw)
161
+ : FANTASY_PLAYER_STATS_LIMIT;
162
+ const before = await readStore(storeMode, dbPath);
163
+ console.error(before.competitions.length === 0
164
+ ? "Cache empty — running full fantasy sync…"
165
+ : `Updating ${before.competitions.length} cached competition(s)…`);
166
+ const result = await updateCachedCompetitions(before, {
167
+ includePlayerStats: withPlayers,
168
+ playerStatsLimit,
169
+ latestSeasonPlayersOnly: true,
170
+ onProgress: (msg) => console.error(msg),
171
+ });
172
+ const cache = await writeSyncToStore(storeMode, dbPath, result);
173
+ printSyncSummary("statsbomb", result, cache);
174
+ }
175
+ async function cmdPull(args) {
176
+ const url = getFlag(args, "--url") ?? dataBundleUrl(GITLAB_PROJECT_ID);
177
+ console.error(`Pulling data bundle from ${url}…`);
178
+ const res = await fetch(url);
179
+ if (!res.ok) {
180
+ throw new Error(`Failed to pull data bundle (${res.status}). ` +
181
+ `Ensure the GitLab CI cron has published campus-data/latest (see docs/cron.md).`);
182
+ }
183
+ const remote = (await res.json());
184
+ const incoming = {
185
+ ...emptyCache(),
186
+ competitions: remote.competitions ?? [],
187
+ seasons: remote.seasons ?? [],
188
+ teams: remote.teams ?? [],
189
+ matches: remote.matches ?? [],
190
+ identities: remote.identities ?? [],
191
+ players: remote.players ?? [],
192
+ playerMatchStats: remote.playerMatchStats ?? [],
193
+ lineups: remote.lineups ?? [],
194
+ injuries: remote.injuries ?? [],
195
+ };
196
+ const local = await readStore(storeMode, dbPath);
197
+ const merged = mergeSyncResult(local, {
198
+ competitions: incoming.competitions,
199
+ seasons: incoming.seasons,
200
+ teams: incoming.teams,
201
+ matches: incoming.matches,
202
+ players: incoming.players,
203
+ playerMatchStats: incoming.playerMatchStats,
204
+ lineups: incoming.lineups,
205
+ injuries: incoming.injuries,
206
+ });
207
+ // Preserve local identities; prefer remote entity payloads via mergeById.
208
+ merged.identities = local.identities.length
209
+ ? local.identities
210
+ : incoming.identities;
211
+ await writeCacheToStore(storeMode, dbPath, merged);
212
+ console.log(JSON.stringify({
213
+ url,
214
+ cache: storeMode === "json" ? cachePath() : dbPath,
215
+ totals: {
216
+ competitions: merged.competitions.length,
217
+ seasons: merged.seasons.length,
218
+ teams: merged.teams.length,
219
+ matches: merged.matches.length,
220
+ players: merged.players.length,
221
+ playerMatchStats: merged.playerMatchStats.length,
222
+ lineups: merged.lineups.length,
223
+ },
224
+ }, null, 2));
225
+ }
226
+ async function cmdAvailable() {
227
+ const list = await listWomenCompetitions();
228
+ console.log(JSON.stringify(list.map((c) => ({
229
+ id: c.id,
230
+ name: c.name,
231
+ country: c.country,
232
+ international: c.international,
233
+ seasons: c.seasons.map((s) => s.name),
234
+ })), null, 2));
235
+ }
236
+ async function cmdCompetitions() {
237
+ const cache = await readStore(storeMode, dbPath);
238
+ console.log(JSON.stringify(cache.competitions.map((c) => ({
239
+ id: c.id,
240
+ name: c.name,
241
+ country: c.country,
242
+ international: c.international ?? false,
243
+ })), null, 2));
244
+ }
245
+ async function cmdSeasons(args) {
246
+ const name = getFlag(args, "--competition");
247
+ if (!name)
248
+ usage();
249
+ const cache = await readStore(storeMode, dbPath);
250
+ const competition = requireCompetition(cache, name);
251
+ const seasons = cache.seasons.filter((s) => s.competitionId === competition.id);
252
+ console.log(JSON.stringify(seasons.map((s) => ({ id: s.id, name: s.name, competition: competition.name })), null, 2));
253
+ }
254
+ async function cmdTeams(args) {
255
+ const name = getFlag(args, "--competition");
256
+ if (!name)
257
+ usage();
258
+ const cache = await readStore(storeMode, dbPath);
259
+ const competition = requireCompetition(cache, name);
260
+ const seasonIds = new Set(cache.seasons
261
+ .filter((s) => s.competitionId === competition.id)
262
+ .map((s) => s.id));
263
+ const teamIds = new Set();
264
+ for (const m of cache.matches) {
265
+ if (m.competitionId === competition.id && seasonIds.has(m.seasonId)) {
266
+ teamIds.add(m.homeTeamId);
267
+ teamIds.add(m.awayTeamId);
268
+ }
269
+ }
270
+ const teams = cache.teams
271
+ .filter((t) => teamIds.has(t.id))
272
+ .sort((a, b) => a.name.localeCompare(b.name));
273
+ console.log(JSON.stringify(teams.map((t) => ({
274
+ id: t.id,
275
+ name: t.name,
276
+ country: t.country,
277
+ kind: t.kind,
278
+ })), null, 2));
279
+ }
280
+ async function cmdMatches(args) {
281
+ const competitionName = getFlag(args, "--competition");
282
+ if (!competitionName)
283
+ usage();
284
+ const seasonName = getFlag(args, "--season");
285
+ const teamName = getFlag(args, "--team");
286
+ const cache = await readStore(storeMode, dbPath);
287
+ const competition = requireCompetition(cache, competitionName);
288
+ let seasonId;
289
+ if (seasonName) {
290
+ const season = cache.seasons.find((s) => s.competitionId === competition.id &&
291
+ s.name.toLowerCase() === seasonName.toLowerCase());
292
+ if (!season) {
293
+ throw new Error(`Season not in cache: "${seasonName}"`);
294
+ }
295
+ seasonId = season.id;
296
+ }
297
+ const teamById = new Map(cache.teams.map((t) => [t.id, t]));
298
+ const seasonById = new Map(cache.seasons.map((s) => [s.id, s]));
299
+ const matches = cache.matches.filter((m) => {
300
+ if (m.competitionId !== competition.id)
301
+ return false;
302
+ if (seasonId && m.seasonId !== seasonId)
303
+ return false;
304
+ if (teamName) {
305
+ const home = teamById.get(m.homeTeamId)?.name ?? "";
306
+ const away = teamById.get(m.awayTeamId)?.name ?? "";
307
+ if (!includesCI(home, teamName) && !includesCI(away, teamName)) {
308
+ return false;
309
+ }
310
+ }
311
+ return true;
312
+ });
313
+ console.log(JSON.stringify(matches.map((m) => ({
314
+ id: m.id,
315
+ date: m.date,
316
+ season: seasonById.get(m.seasonId)?.name,
317
+ home: teamById.get(m.homeTeamId)?.name,
318
+ away: teamById.get(m.awayTeamId)?.name,
319
+ score: m.homeScore == null || m.awayScore == null
320
+ ? null
321
+ : `${m.homeScore}-${m.awayScore}`,
322
+ })), null, 2));
323
+ }
324
+ async function cmdPlayers(args) {
325
+ const teamName = getFlag(args, "--team");
326
+ const name = getFlag(args, "--name");
327
+ const cache = await readStore(storeMode, dbPath);
328
+ const teamById = new Map(cache.teams.map((t) => [t.id, t]));
329
+ const teamIds = teamName
330
+ ? new Set(cache.teams
331
+ .filter((t) => includesCI(t.name, teamName))
332
+ .map((t) => t.id))
333
+ : null;
334
+ let players = cache.players;
335
+ if (name)
336
+ players = players.filter((p) => includesCI(p.name, name));
337
+ if (teamIds) {
338
+ const playerIds = new Set(cache.playerMatchStats
339
+ .filter((s) => teamIds.has(s.teamId))
340
+ .map((s) => s.playerId));
341
+ players = players.filter((p) => playerIds.has(p.id));
342
+ }
343
+ console.log(JSON.stringify(players.map((p) => ({
344
+ id: p.id,
345
+ name: p.name,
346
+ nickname: p.nickname,
347
+ country: p.country,
348
+ })), null, 2));
349
+ }
350
+ async function cmdPlayerStats(args) {
351
+ const competitionName = getFlag(args, "--competition");
352
+ const matchId = getFlag(args, "--match");
353
+ const playerName = getFlag(args, "--player");
354
+ const teamName = getFlag(args, "--team");
355
+ const cache = await readStore(storeMode, dbPath);
356
+ let competitionId;
357
+ if (competitionName) {
358
+ competitionId = requireCompetition(cache, competitionName).id;
359
+ }
360
+ const matchIds = new Set(cache.matches
361
+ .filter((m) => (competitionId ? m.competitionId === competitionId : true))
362
+ .filter((m) => (matchId ? m.id === matchId || m.id.endsWith(`:${matchId}`) : true))
363
+ .map((m) => m.id));
364
+ const playerById = new Map(cache.players.map((p) => [p.id, p]));
365
+ const teamById = new Map(cache.teams.map((t) => [t.id, t]));
366
+ const rows = cache.playerMatchStats.filter((s) => {
367
+ if (!matchIds.has(s.matchId))
368
+ return false;
369
+ if (playerName) {
370
+ const n = playerById.get(s.playerId)?.name ?? "";
371
+ if (!includesCI(n, playerName))
372
+ return false;
373
+ }
374
+ if (teamName) {
375
+ const n = teamById.get(s.teamId)?.name ?? "";
376
+ if (!includesCI(n, teamName))
377
+ return false;
378
+ }
379
+ return true;
380
+ });
381
+ console.log(JSON.stringify(rows.map((s) => ({
382
+ matchId: s.matchId,
383
+ player: playerById.get(s.playerId)?.name,
384
+ team: teamById.get(s.teamId)?.name,
385
+ minutes: s.minutes,
386
+ goals: s.goals,
387
+ assists: s.assists,
388
+ yellowCards: s.yellowCards,
389
+ redCards: s.redCards,
390
+ })), null, 2));
391
+ }
392
+ async function cmdIdentities(args) {
393
+ const sub = args[0];
394
+ if (sub === "propose") {
395
+ const competition = getFlag(args, "--competition");
396
+ if (!competition)
397
+ usage();
398
+ const cache = proposeIdentitiesForCompetition(await readStore(storeMode, dbPath), competition);
399
+ await writeCacheToStore(storeMode, dbPath, cache);
400
+ console.log(JSON.stringify({
401
+ competition,
402
+ identities: cache.identities.length,
403
+ pending: cache.identities.filter((i) => i.status === "pending").length,
404
+ resolved: cache.identities.filter((i) => i.status === "resolved").length,
405
+ }, null, 2));
406
+ return;
407
+ }
408
+ if (sub === "confirm" || sub === "reject") {
409
+ const id = getFlag(args, "--id");
410
+ if (!id)
411
+ usage();
412
+ const status = sub === "confirm" ? "resolved" : "rejected";
413
+ const before = await readStore(storeMode, dbPath);
414
+ if (!before.identities.some((i) => i.id === id)) {
415
+ throw new Error(`Identity not found: ${id}`);
416
+ }
417
+ const cache = setIdentityStatus(before, id, status);
418
+ await writeCacheToStore(storeMode, dbPath, cache);
419
+ console.log(JSON.stringify(cache.identities.find((i) => i.id === id), null, 2));
420
+ return;
421
+ }
422
+ const status = getFlag(args, "--status");
423
+ const cache = await readStore(storeMode, dbPath);
424
+ const identities = cache.identities.filter((i) => status ? i.status === status : true);
425
+ console.log(JSON.stringify(identities, null, 2));
426
+ }
427
+ async function cmdLineups(args) {
428
+ const matchId = getFlag(args, "--match");
429
+ const team = getFlag(args, "--team");
430
+ const client = CampusClient.fromCache(await readStore(storeMode, dbPath));
431
+ const playerById = new Map(client.data.players.map((p) => [p.id, p]));
432
+ const teamById = new Map(client.data.teams.map((t) => [t.id, t]));
433
+ console.log(JSON.stringify(client.lineups({ matchId, team }).map((l) => ({
434
+ matchId: l.matchId,
435
+ team: teamById.get(l.teamId)?.name,
436
+ player: playerById.get(l.playerId)?.name,
437
+ started: l.started,
438
+ jerseyNumber: l.jerseyNumber,
439
+ })), null, 2));
440
+ }
441
+ async function cmdSquad(args) {
442
+ const competition = getFlag(args, "--competition");
443
+ const team = getFlag(args, "--team");
444
+ if (!competition || !team)
445
+ usage();
446
+ const season = getFlag(args, "--season");
447
+ const client = CampusClient.fromCache(await readStore(storeMode, dbPath));
448
+ console.log(JSON.stringify(client.squad({ competition, team, season }), null, 2));
449
+ }
450
+ async function cmdFantasyPoints(args) {
451
+ const client = CampusClient.fromCache(await readStore(storeMode, dbPath));
452
+ const rows = client.fantasyPoints({
453
+ competition: getFlag(args, "--competition"),
454
+ matchId: getFlag(args, "--match"),
455
+ player: getFlag(args, "--player"),
456
+ team: getFlag(args, "--team"),
457
+ });
458
+ const playerById = new Map(client.data.players.map((p) => [p.id, p]));
459
+ console.log(JSON.stringify(rows.map((r) => ({
460
+ matchId: r.matchId,
461
+ player: playerById.get(r.playerId)?.name,
462
+ points: r.points,
463
+ breakdown: r.breakdown,
464
+ })), null, 2));
465
+ }
466
+ async function cmdInjuries() {
467
+ const client = CampusClient.fromCache(await readStore(storeMode, dbPath));
468
+ console.log(JSON.stringify({
469
+ available: client.injuries().available,
470
+ message: INJURIES_STATUS_MESSAGE,
471
+ records: client.injuries().records,
472
+ }, null, 2));
473
+ }
474
+ async function main() {
475
+ const [, , command, ...args] = process.argv;
476
+ if (!command || command === "--help" || args.includes("--help"))
477
+ usage();
478
+ const allArgs = process.argv.slice(2);
479
+ storeMode = resolveStoreMode(allArgs);
480
+ dbPath = resolveDbPath(allArgs);
481
+ switch (command) {
482
+ case "sync":
483
+ await cmdSync(args);
484
+ break;
485
+ case "update":
486
+ await cmdUpdate(args);
487
+ break;
488
+ case "pull":
489
+ await cmdPull(args);
490
+ break;
491
+ case "available":
492
+ await cmdAvailable();
493
+ break;
494
+ case "competitions":
495
+ await cmdCompetitions();
496
+ break;
497
+ case "seasons":
498
+ await cmdSeasons(args);
499
+ break;
500
+ case "teams":
501
+ await cmdTeams(args);
502
+ break;
503
+ case "matches":
504
+ await cmdMatches(args);
505
+ break;
506
+ case "players":
507
+ await cmdPlayers(args);
508
+ break;
509
+ case "player-stats":
510
+ await cmdPlayerStats(args);
511
+ break;
512
+ case "lineups":
513
+ await cmdLineups(args);
514
+ break;
515
+ case "squad":
516
+ await cmdSquad(args);
517
+ break;
518
+ case "fantasy-points":
519
+ await cmdFantasyPoints(args);
520
+ break;
521
+ case "injuries":
522
+ await cmdInjuries();
523
+ break;
524
+ case "identities":
525
+ await cmdIdentities(args);
526
+ break;
527
+ case "migrate":
528
+ await runMigrate(args);
529
+ break;
530
+ default:
531
+ usage();
532
+ }
533
+ }
534
+ main().catch((err) => {
535
+ console.error(err instanceof Error ? err.message : err);
536
+ process.exit(1);
537
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * App-facing client: install `campus` and use this from a web API,
3
+ * serverless function, or Node backend. Browser bundles can import query
4
+ * helpers against an in-memory cache loaded via `fromBundle` / `fromCache`.
5
+ */
6
+ import { type FantasySyncOptions } from "./fantasy.js";
7
+ import { type FantasyPointRow, type FantasyScoringRules } from "./scoring.js";
8
+ import type { CampusCache, Competition, InjuryRecord, LineupEntry, Match, Player, PlayerMatchStats, Season, Team } from "./types.js";
9
+ export interface SquadMember {
10
+ playerId: string;
11
+ playerName: string;
12
+ teamId: string;
13
+ teamName: string;
14
+ appearances: number;
15
+ }
16
+ export declare class CampusClient {
17
+ private cache;
18
+ private readonly persistPath?;
19
+ private constructor();
20
+ /** Open local JSON cache (default `.campus/cache.json`). */
21
+ static open(filePath?: string): Promise<CampusClient>;
22
+ /** Use an in-memory cache (e.g. after fetch in an API route). */
23
+ static fromCache(cache: CampusCache): CampusClient;
24
+ /** Download the cron-published bundle and optionally merge with local cache. */
25
+ static fromBundle(options?: {
26
+ url?: string;
27
+ mergeWith?: CampusCache;
28
+ persistPath?: string;
29
+ }): Promise<CampusClient>;
30
+ /** Snapshot of normalized data (safe to serialize to the browser). */
31
+ get data(): CampusCache;
32
+ save(filePath?: string): Promise<void>;
33
+ /** Sync every women's StatsBomb competition (clubs + national teams). */
34
+ syncFantasy(options?: FantasySyncOptions): Promise<CampusCache>;
35
+ /** Re-sync competitions already present in the cache. */
36
+ update(options?: FantasySyncOptions): Promise<CampusCache>;
37
+ /** Pull the published cron bundle into this client. */
38
+ pull(url?: string): Promise<CampusCache>;
39
+ competitions(filter?: {
40
+ name?: string;
41
+ international?: boolean;
42
+ }): Competition[];
43
+ seasons(filter?: {
44
+ competition?: string;
45
+ }): Season[];
46
+ teams(filter?: {
47
+ competition?: string;
48
+ kind?: Team["kind"];
49
+ name?: string;
50
+ }): Team[];
51
+ matches(filter?: {
52
+ competition?: string;
53
+ season?: string;
54
+ team?: string;
55
+ }): Match[];
56
+ players(filter?: {
57
+ name?: string;
58
+ team?: string;
59
+ }): Player[];
60
+ playerStats(filter?: {
61
+ competition?: string;
62
+ matchId?: string;
63
+ player?: string;
64
+ team?: string;
65
+ }): PlayerMatchStats[];
66
+ lineups(filter?: {
67
+ matchId?: string;
68
+ team?: string;
69
+ }): LineupEntry[];
70
+ /** Season squad = distinct players who appeared in lineups for a team. */
71
+ squad(filter: {
72
+ competition: string;
73
+ team: string;
74
+ season?: string;
75
+ }): SquadMember[];
76
+ fantasyPoints(filter?: {
77
+ competition?: string;
78
+ matchId?: string;
79
+ player?: string;
80
+ team?: string;
81
+ rules?: FantasyScoringRules;
82
+ }): FantasyPointRow[];
83
+ injuries(): {
84
+ available: boolean;
85
+ message: string;
86
+ records: InjuryRecord[];
87
+ };
88
+ }