fitzroy 1.0.0 → 1.0.2

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.js CHANGED
@@ -1,88 +1,2508 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- __require
4
- } from "./shared/chunk-xv8z2kms.js";
5
-
6
- // src/cli.ts
7
- import { defineCommand, runMain } from "citty";
8
- import pc from "picocolors";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
9
11
 
10
12
  // src/lib/errors.ts
11
- class AflApiError extends Error {
12
- statusCode;
13
- name = "AflApiError";
14
- constructor(message, statusCode) {
15
- super(message);
16
- this.statusCode = statusCode;
13
+ var AflApiError, ScrapeError, UnsupportedSourceError, ValidationError;
14
+ var init_errors = __esm({
15
+ "src/lib/errors.ts"() {
16
+ "use strict";
17
+ AflApiError = class extends Error {
18
+ constructor(message, statusCode) {
19
+ super(message);
20
+ this.statusCode = statusCode;
21
+ }
22
+ name = "AflApiError";
23
+ };
24
+ ScrapeError = class extends Error {
25
+ constructor(message, source) {
26
+ super(message);
27
+ this.source = source;
28
+ }
29
+ name = "ScrapeError";
30
+ };
31
+ UnsupportedSourceError = class extends Error {
32
+ constructor(message, source) {
33
+ super(message);
34
+ this.source = source;
35
+ }
36
+ name = "UnsupportedSourceError";
37
+ };
38
+ ValidationError = class extends Error {
39
+ constructor(message, issues) {
40
+ super(message);
41
+ this.issues = issues;
42
+ }
43
+ name = "ValidationError";
44
+ };
45
+ }
46
+ });
47
+
48
+ // src/lib/result.ts
49
+ function ok(data) {
50
+ return { success: true, data };
51
+ }
52
+ function err(error) {
53
+ return { success: false, error };
54
+ }
55
+ var init_result = __esm({
56
+ "src/lib/result.ts"() {
57
+ "use strict";
58
+ }
59
+ });
60
+
61
+ // src/lib/team-mapping.ts
62
+ function normaliseTeamName(raw) {
63
+ const trimmed = raw.trim();
64
+ return ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
65
+ }
66
+ var TEAM_ALIASES, ALIAS_MAP;
67
+ var init_team_mapping = __esm({
68
+ "src/lib/team-mapping.ts"() {
69
+ "use strict";
70
+ TEAM_ALIASES = [
71
+ ["Adelaide Crows", "Adelaide", "Crows", "ADEL", "AD"],
72
+ ["Brisbane Lions", "Brisbane", "Brisbane Bears", "Bears", "Fitzroy Lions", "BL", "BRIS"],
73
+ ["Carlton", "Carlton Blues", "Blues", "CARL", "CA"],
74
+ ["Collingwood", "Collingwood Magpies", "Magpies", "COLL", "CW"],
75
+ ["Essendon", "Essendon Bombers", "Bombers", "ESS", "ES"],
76
+ ["Fremantle", "Fremantle Dockers", "Dockers", "FRE", "FR"],
77
+ ["Geelong Cats", "Geelong", "Cats", "GEEL", "GE"],
78
+ [
79
+ "Gold Coast Suns",
80
+ "Gold Coast",
81
+ "Gold Coast SUNS",
82
+ "Gold Coast Football Club",
83
+ "Suns",
84
+ "GCFC",
85
+ "GC"
86
+ ],
87
+ [
88
+ "GWS Giants",
89
+ "GWS",
90
+ "GWS GIANTS",
91
+ "Greater Western Sydney",
92
+ "Giants",
93
+ "Greater Western Sydney Giants",
94
+ "GW"
95
+ ],
96
+ ["Hawthorn", "Hawthorn Hawks", "Hawks", "HAW", "HW"],
97
+ ["Melbourne", "Melbourne Demons", "Demons", "MELB", "ME"],
98
+ ["North Melbourne", "North Melbourne Kangaroos", "Kangaroos", "Kangas", "North", "NMFC", "NM"],
99
+ ["Port Adelaide", "Port Adelaide Power", "Power", "Port", "PA", "PAFC"],
100
+ ["Richmond", "Richmond Tigers", "Tigers", "RICH", "RI"],
101
+ ["St Kilda", "St Kilda Saints", "Saints", "Saint Kilda", "STK", "SK"],
102
+ ["Sydney Swans", "Sydney", "Swans", "South Melbourne", "South Melbourne Swans", "SYD", "SY"],
103
+ ["West Coast Eagles", "West Coast", "Eagles", "WCE", "WC"],
104
+ ["Western Bulldogs", "Bulldogs", "Footscray", "Footscray Bulldogs", "WB", "WBD"],
105
+ // Historical / defunct VFL teams
106
+ ["Fitzroy", "Fitzroy Reds", "Fitzroy Gorillas", "Fitzroy Maroons", "FI"],
107
+ ["University", "University Blacks"]
108
+ ];
109
+ ALIAS_MAP = (() => {
110
+ const map = /* @__PURE__ */ new Map();
111
+ for (const [canonical, ...aliases] of TEAM_ALIASES) {
112
+ map.set(canonical.toLowerCase(), canonical);
113
+ for (const alias of aliases) {
114
+ map.set(alias.toLowerCase(), canonical);
115
+ }
116
+ }
117
+ return map;
118
+ })();
119
+ }
120
+ });
121
+
122
+ // src/lib/validation.ts
123
+ import { z } from "zod/v4";
124
+ var AflApiTokenSchema, CompetitionSchema, CompetitionListSchema, CompseasonSchema, CompseasonListSchema, RoundSchema, RoundListSchema, ScoreSchema, PeriodScoreSchema, TeamScoreSchema, CfsMatchTeamSchema, CfsMatchSchema, CfsScoreSchema, CfsVenueSchema, MatchItemSchema, MatchItemListSchema, CfsPlayerInnerSchema, PlayerGameStatsSchema, PlayerStatsItemSchema, PlayerStatsListSchema, RosterPlayerSchema, TeamPlayersSchema, MatchRosterSchema, TeamItemSchema, TeamListSchema, SquadPlayerInnerSchema, SquadPlayerItemSchema, SquadSchema, SquadListSchema, WinLossRecordSchema, LadderEntryRawSchema, LadderResponseSchema;
125
+ var init_validation = __esm({
126
+ "src/lib/validation.ts"() {
127
+ "use strict";
128
+ AflApiTokenSchema = z.object({
129
+ token: z.string(),
130
+ disclaimer: z.string().optional()
131
+ }).passthrough();
132
+ CompetitionSchema = z.object({
133
+ id: z.number(),
134
+ name: z.string(),
135
+ code: z.string().optional()
136
+ }).passthrough();
137
+ CompetitionListSchema = z.object({
138
+ competitions: z.array(CompetitionSchema)
139
+ }).passthrough();
140
+ CompseasonSchema = z.object({
141
+ id: z.number(),
142
+ name: z.string(),
143
+ shortName: z.string().optional(),
144
+ currentRoundNumber: z.number().optional()
145
+ }).passthrough();
146
+ CompseasonListSchema = z.object({
147
+ compSeasons: z.array(CompseasonSchema)
148
+ }).passthrough();
149
+ RoundSchema = z.object({
150
+ id: z.number(),
151
+ /** Provider ID used by /cfs/ endpoints (e.g. "CD_R202501401"). */
152
+ providerId: z.string().optional(),
153
+ name: z.string(),
154
+ abbreviation: z.string().optional(),
155
+ roundNumber: z.number(),
156
+ utcStartTime: z.string().optional(),
157
+ utcEndTime: z.string().optional()
158
+ }).passthrough();
159
+ RoundListSchema = z.object({
160
+ rounds: z.array(RoundSchema)
161
+ }).passthrough();
162
+ ScoreSchema = z.object({
163
+ totalScore: z.number(),
164
+ goals: z.number(),
165
+ behinds: z.number(),
166
+ superGoals: z.number().nullable().optional()
167
+ }).passthrough();
168
+ PeriodScoreSchema = z.object({
169
+ periodNumber: z.number(),
170
+ score: ScoreSchema
171
+ }).passthrough();
172
+ TeamScoreSchema = z.object({
173
+ matchScore: ScoreSchema,
174
+ periodScore: z.array(PeriodScoreSchema).optional(),
175
+ rushedBehinds: z.number().optional(),
176
+ minutesInFront: z.number().optional()
177
+ }).passthrough();
178
+ CfsMatchTeamSchema = z.object({
179
+ name: z.string(),
180
+ teamId: z.string(),
181
+ abbr: z.string().optional(),
182
+ nickname: z.string().optional()
183
+ }).passthrough();
184
+ CfsMatchSchema = z.object({
185
+ matchId: z.string(),
186
+ name: z.string().optional(),
187
+ status: z.string(),
188
+ utcStartTime: z.string(),
189
+ homeTeamId: z.string(),
190
+ awayTeamId: z.string(),
191
+ homeTeam: CfsMatchTeamSchema,
192
+ awayTeam: CfsMatchTeamSchema,
193
+ round: z.string().optional(),
194
+ abbr: z.string().optional()
195
+ }).passthrough();
196
+ CfsScoreSchema = z.object({
197
+ status: z.string(),
198
+ matchId: z.string(),
199
+ homeTeamScore: TeamScoreSchema,
200
+ awayTeamScore: TeamScoreSchema
201
+ }).passthrough();
202
+ CfsVenueSchema = z.object({
203
+ name: z.string(),
204
+ venueId: z.string().optional(),
205
+ state: z.string().optional(),
206
+ timeZone: z.string().optional()
207
+ }).passthrough();
208
+ MatchItemSchema = z.object({
209
+ match: CfsMatchSchema,
210
+ score: CfsScoreSchema.optional(),
211
+ venue: CfsVenueSchema.optional(),
212
+ round: z.object({
213
+ name: z.string(),
214
+ roundId: z.string(),
215
+ roundNumber: z.number()
216
+ }).passthrough().optional()
217
+ }).passthrough();
218
+ MatchItemListSchema = z.object({
219
+ roundId: z.string().optional(),
220
+ items: z.array(MatchItemSchema)
221
+ }).passthrough();
222
+ CfsPlayerInnerSchema = z.object({
223
+ playerId: z.string(),
224
+ playerName: z.object({
225
+ givenName: z.string(),
226
+ surname: z.string()
227
+ }).passthrough(),
228
+ captain: z.boolean().optional(),
229
+ playerJumperNumber: z.number().optional()
230
+ }).passthrough();
231
+ PlayerGameStatsSchema = z.object({
232
+ goals: z.number().optional(),
233
+ behinds: z.number().optional(),
234
+ kicks: z.number().optional(),
235
+ handballs: z.number().optional(),
236
+ disposals: z.number().optional(),
237
+ marks: z.number().optional(),
238
+ bounces: z.number().optional(),
239
+ tackles: z.number().optional(),
240
+ contestedPossessions: z.number().optional(),
241
+ uncontestedPossessions: z.number().optional(),
242
+ totalPossessions: z.number().optional(),
243
+ inside50s: z.number().optional(),
244
+ marksInside50: z.number().optional(),
245
+ contestedMarks: z.number().optional(),
246
+ hitouts: z.number().optional(),
247
+ onePercenters: z.number().optional(),
248
+ disposalEfficiency: z.number().optional(),
249
+ clangers: z.number().optional(),
250
+ freesFor: z.number().optional(),
251
+ freesAgainst: z.number().optional(),
252
+ dreamTeamPoints: z.number().optional(),
253
+ clearances: z.object({
254
+ centreClearances: z.number().optional(),
255
+ stoppageClearances: z.number().optional(),
256
+ totalClearances: z.number().optional()
257
+ }).passthrough().optional(),
258
+ rebound50s: z.number().optional(),
259
+ goalAssists: z.number().optional(),
260
+ goalAccuracy: z.number().optional(),
261
+ turnovers: z.number().optional(),
262
+ intercepts: z.number().optional(),
263
+ tacklesInside50: z.number().optional(),
264
+ shotsAtGoal: z.number().optional(),
265
+ metresGained: z.number().optional(),
266
+ scoreInvolvements: z.number().optional(),
267
+ ratingPoints: z.number().optional(),
268
+ extendedStats: z.object({
269
+ effectiveDisposals: z.number().optional(),
270
+ effectiveKicks: z.number().optional(),
271
+ kickEfficiency: z.number().optional(),
272
+ kickToHandballRatio: z.number().optional(),
273
+ pressureActs: z.number().optional(),
274
+ defHalfPressureActs: z.number().optional(),
275
+ spoils: z.number().optional(),
276
+ hitoutsToAdvantage: z.number().optional(),
277
+ hitoutWinPercentage: z.number().optional(),
278
+ hitoutToAdvantageRate: z.number().optional(),
279
+ groundBallGets: z.number().optional(),
280
+ f50GroundBallGets: z.number().optional(),
281
+ interceptMarks: z.number().optional(),
282
+ marksOnLead: z.number().optional(),
283
+ contestedPossessionRate: z.number().optional(),
284
+ contestOffOneOnOnes: z.number().optional(),
285
+ contestOffWins: z.number().optional(),
286
+ contestOffWinsPercentage: z.number().optional(),
287
+ contestDefOneOnOnes: z.number().optional(),
288
+ contestDefLosses: z.number().optional(),
289
+ contestDefLossPercentage: z.number().optional(),
290
+ centreBounceAttendances: z.number().optional(),
291
+ kickins: z.number().optional(),
292
+ kickinsPlayon: z.number().optional(),
293
+ ruckContests: z.number().optional(),
294
+ scoreLaunches: z.number().optional()
295
+ }).passthrough().optional()
296
+ }).passthrough();
297
+ PlayerStatsItemSchema = z.object({
298
+ player: z.object({
299
+ player: z.object({
300
+ position: z.string().optional(),
301
+ player: CfsPlayerInnerSchema
302
+ }).passthrough(),
303
+ jumperNumber: z.number().optional()
304
+ }).passthrough(),
305
+ teamId: z.string(),
306
+ playerStats: z.object({
307
+ stats: PlayerGameStatsSchema,
308
+ timeOnGroundPercentage: z.number().optional()
309
+ }).passthrough()
310
+ }).passthrough();
311
+ PlayerStatsListSchema = z.object({
312
+ homeTeamPlayerStats: z.array(PlayerStatsItemSchema),
313
+ awayTeamPlayerStats: z.array(PlayerStatsItemSchema)
314
+ }).passthrough();
315
+ RosterPlayerSchema = z.object({
316
+ player: z.object({
317
+ position: z.string().optional(),
318
+ player: CfsPlayerInnerSchema
319
+ }).passthrough(),
320
+ jumperNumber: z.number().optional()
321
+ }).passthrough();
322
+ TeamPlayersSchema = z.object({
323
+ teamId: z.string(),
324
+ players: z.array(RosterPlayerSchema)
325
+ }).passthrough();
326
+ MatchRosterSchema = z.object({
327
+ match: CfsMatchSchema,
328
+ teamPlayers: z.array(TeamPlayersSchema)
329
+ }).passthrough();
330
+ TeamItemSchema = z.object({
331
+ id: z.number(),
332
+ name: z.string(),
333
+ abbreviation: z.string().optional(),
334
+ teamType: z.string().optional()
335
+ }).passthrough();
336
+ TeamListSchema = z.object({
337
+ teams: z.array(TeamItemSchema)
338
+ }).passthrough();
339
+ SquadPlayerInnerSchema = z.object({
340
+ id: z.number(),
341
+ providerId: z.string().optional(),
342
+ firstName: z.string(),
343
+ surname: z.string(),
344
+ dateOfBirth: z.string().optional(),
345
+ heightInCm: z.number().optional(),
346
+ weightInKg: z.number().optional(),
347
+ draftYear: z.string().optional(),
348
+ draftPosition: z.string().optional(),
349
+ draftType: z.string().optional(),
350
+ debutYear: z.string().optional(),
351
+ recruitedFrom: z.string().optional()
352
+ }).passthrough();
353
+ SquadPlayerItemSchema = z.object({
354
+ player: SquadPlayerInnerSchema,
355
+ jumperNumber: z.number().optional(),
356
+ position: z.string().optional()
357
+ }).passthrough();
358
+ SquadSchema = z.object({
359
+ team: z.object({
360
+ name: z.string()
361
+ }).passthrough().optional(),
362
+ players: z.array(SquadPlayerItemSchema)
363
+ }).passthrough();
364
+ SquadListSchema = z.object({
365
+ squad: SquadSchema
366
+ }).passthrough();
367
+ WinLossRecordSchema = z.object({
368
+ wins: z.number(),
369
+ losses: z.number(),
370
+ draws: z.number(),
371
+ played: z.number().optional()
372
+ }).passthrough();
373
+ LadderEntryRawSchema = z.object({
374
+ position: z.number(),
375
+ team: z.object({
376
+ name: z.string(),
377
+ id: z.number().optional(),
378
+ abbreviation: z.string().optional()
379
+ }).passthrough(),
380
+ played: z.number().optional(),
381
+ pointsFor: z.number().optional(),
382
+ pointsAgainst: z.number().optional(),
383
+ thisSeasonRecord: z.object({
384
+ aggregatePoints: z.number().optional(),
385
+ percentage: z.number().optional(),
386
+ winLossRecord: WinLossRecordSchema.optional()
387
+ }).passthrough().optional(),
388
+ form: z.string().optional()
389
+ }).passthrough();
390
+ LadderResponseSchema = z.object({
391
+ ladders: z.array(
392
+ z.object({
393
+ entries: z.array(LadderEntryRawSchema)
394
+ }).passthrough()
395
+ ),
396
+ round: z.object({
397
+ roundNumber: z.number(),
398
+ name: z.string().optional()
399
+ }).passthrough().optional()
400
+ }).passthrough();
401
+ }
402
+ });
403
+
404
+ // src/sources/afl-api.ts
405
+ var TOKEN_URL, API_BASE, CFS_BASE, AflApiClient;
406
+ var init_afl_api = __esm({
407
+ "src/sources/afl-api.ts"() {
408
+ "use strict";
409
+ init_errors();
410
+ init_result();
411
+ init_validation();
412
+ TOKEN_URL = "https://api.afl.com.au/cfs/afl/WMCTok";
413
+ API_BASE = "https://aflapi.afl.com.au/afl/v2";
414
+ CFS_BASE = "https://api.afl.com.au/cfs/afl";
415
+ AflApiClient = class {
416
+ fetchFn;
417
+ tokenUrl;
418
+ cachedToken = null;
419
+ constructor(options) {
420
+ this.fetchFn = options?.fetchFn ?? globalThis.fetch;
421
+ this.tokenUrl = options?.tokenUrl ?? TOKEN_URL;
422
+ }
423
+ /**
424
+ * Authenticate with the WMCTok token endpoint and cache the token.
425
+ *
426
+ * @returns The access token on success, or an error Result.
427
+ */
428
+ async authenticate() {
429
+ try {
430
+ const response = await this.fetchFn(this.tokenUrl, {
431
+ method: "POST",
432
+ headers: { "Content-Length": "0" }
433
+ });
434
+ if (!response.ok) {
435
+ return err(new AflApiError(`Token request failed: ${response.status}`, response.status));
436
+ }
437
+ const json = await response.json();
438
+ const parsed = AflApiTokenSchema.safeParse(json);
439
+ if (!parsed.success) {
440
+ return err(new AflApiError("Invalid token response format"));
441
+ }
442
+ const ttlMs = 30 * 60 * 1e3;
443
+ this.cachedToken = {
444
+ accessToken: parsed.data.token,
445
+ expiresAt: Date.now() + ttlMs
446
+ };
447
+ return ok(parsed.data.token);
448
+ } catch (cause) {
449
+ return err(
450
+ new AflApiError(
451
+ `Token request failed: ${cause instanceof Error ? cause.message : String(cause)}`
452
+ )
453
+ );
454
+ }
455
+ }
456
+ /**
457
+ * Whether the cached token is still valid (not expired).
458
+ */
459
+ get isAuthenticated() {
460
+ return this.cachedToken !== null && Date.now() < this.cachedToken.expiresAt;
461
+ }
462
+ /**
463
+ * Perform an authenticated fetch, automatically adding the bearer token.
464
+ * Retries once on 401 by re-authenticating.
465
+ *
466
+ * @param url - The URL to fetch.
467
+ * @param init - Additional fetch options.
468
+ * @returns The Response on success, or an error Result.
469
+ */
470
+ async authedFetch(url, init) {
471
+ if (!this.isAuthenticated) {
472
+ const authResult = await this.authenticate();
473
+ if (!authResult.success) {
474
+ return authResult;
475
+ }
476
+ }
477
+ const doFetch = async () => {
478
+ const token = this.cachedToken;
479
+ if (!token) {
480
+ throw new AflApiError("No cached token available");
481
+ }
482
+ const headers = new Headers(init?.headers);
483
+ headers.set("x-media-mis-token", token.accessToken);
484
+ return this.fetchFn(url, { ...init, headers });
485
+ };
486
+ try {
487
+ let response = await doFetch();
488
+ if (response.status === 401) {
489
+ const authResult = await this.authenticate();
490
+ if (!authResult.success) {
491
+ return authResult;
492
+ }
493
+ response = await doFetch();
494
+ }
495
+ if (!response.ok) {
496
+ return err(
497
+ new AflApiError(
498
+ `Request failed: ${response.status} ${response.statusText}`,
499
+ response.status
500
+ )
501
+ );
502
+ }
503
+ return ok(response);
504
+ } catch (cause) {
505
+ return err(
506
+ new AflApiError(
507
+ `Request failed: ${cause instanceof Error ? cause.message : String(cause)}`
508
+ )
509
+ );
510
+ }
511
+ }
512
+ /**
513
+ * Fetch JSON from a URL, validate with a Zod schema, and return a typed Result.
514
+ *
515
+ * @param url - The URL to fetch.
516
+ * @param schema - Zod schema to validate the response against.
517
+ * @returns Validated data on success, or an error Result.
518
+ */
519
+ async fetchJson(url, schema) {
520
+ const isPublic = url.startsWith(API_BASE);
521
+ let response;
522
+ if (isPublic) {
523
+ try {
524
+ response = await this.fetchFn(url);
525
+ if (!response.ok) {
526
+ return err(
527
+ new AflApiError(
528
+ `Request failed: ${response.status} ${response.statusText}`,
529
+ response.status
530
+ )
531
+ );
532
+ }
533
+ } catch (cause) {
534
+ return err(
535
+ new AflApiError(
536
+ `Request failed: ${cause instanceof Error ? cause.message : String(cause)}`
537
+ )
538
+ );
539
+ }
540
+ } else {
541
+ const fetchResult = await this.authedFetch(url);
542
+ if (!fetchResult.success) {
543
+ return fetchResult;
544
+ }
545
+ response = fetchResult.data;
546
+ }
547
+ try {
548
+ const json = await response.json();
549
+ const parsed = schema.safeParse(json);
550
+ if (!parsed.success) {
551
+ return err(
552
+ new ValidationError("Response validation failed", [
553
+ { path: url, message: String(parsed.error) }
554
+ ])
555
+ );
556
+ }
557
+ return ok(parsed.data);
558
+ } catch (cause) {
559
+ return err(
560
+ new AflApiError(
561
+ `JSON parse failed: ${cause instanceof Error ? cause.message : String(cause)}`
562
+ )
563
+ );
564
+ }
565
+ }
566
+ /**
567
+ * Resolve a competition code (e.g. "AFLM") to its API competition ID.
568
+ *
569
+ * @param code - The competition code to resolve.
570
+ * @returns The competition ID string on success.
571
+ */
572
+ async resolveCompetitionId(code) {
573
+ const result = await this.fetchJson(
574
+ `${API_BASE}/competitions?pageSize=50`,
575
+ CompetitionListSchema
576
+ );
577
+ if (!result.success) {
578
+ return result;
579
+ }
580
+ const apiCode = code === "AFLM" ? "AFL" : code;
581
+ const competition = result.data.competitions.find((c) => c.code === apiCode);
582
+ if (!competition) {
583
+ return err(new AflApiError(`Competition not found for code: ${code}`));
584
+ }
585
+ return ok(competition.id);
586
+ }
587
+ /**
588
+ * Resolve a season (compseason) ID from a competition ID and year.
589
+ *
590
+ * @param competitionId - The competition ID (from {@link resolveCompetitionId}).
591
+ * @param year - The season year (e.g. 2024).
592
+ * @returns The compseason ID string on success.
593
+ */
594
+ async resolveSeasonId(competitionId, year) {
595
+ const result = await this.fetchJson(
596
+ `${API_BASE}/competitions/${competitionId}/compseasons?pageSize=100`,
597
+ CompseasonListSchema
598
+ );
599
+ if (!result.success) {
600
+ return result;
601
+ }
602
+ const yearStr = String(year);
603
+ const season = result.data.compSeasons.find((cs) => cs.name.includes(yearStr));
604
+ if (!season) {
605
+ return err(new AflApiError(`Season not found for year: ${year}`));
606
+ }
607
+ return ok(season.id);
608
+ }
609
+ /**
610
+ * Resolve a season ID from a competition code and year in one step.
611
+ *
612
+ * @param code - The competition code (e.g. "AFLM").
613
+ * @param year - The season year (e.g. 2025).
614
+ * @returns The compseason ID on success.
615
+ */
616
+ async resolveCompSeason(code, year) {
617
+ const compResult = await this.resolveCompetitionId(code);
618
+ if (!compResult.success) return compResult;
619
+ return this.resolveSeasonId(compResult.data, year);
620
+ }
621
+ /**
622
+ * Fetch all rounds for a season with their metadata.
623
+ *
624
+ * @param seasonId - The compseason ID (from {@link resolveSeasonId}).
625
+ * @returns Array of round objects on success.
626
+ */
627
+ async resolveRounds(seasonId) {
628
+ const result = await this.fetchJson(
629
+ `${API_BASE}/compseasons/${seasonId}/rounds?pageSize=50`,
630
+ RoundListSchema
631
+ );
632
+ if (!result.success) {
633
+ return result;
634
+ }
635
+ return ok(result.data.rounds);
636
+ }
637
+ /**
638
+ * Fetch match items for a round using the /cfs/ endpoint.
639
+ *
640
+ * @param roundProviderId - The round provider ID (e.g. "CD_R202501401").
641
+ * @returns Array of match items on success.
642
+ */
643
+ async fetchRoundMatchItems(roundProviderId) {
644
+ const result = await this.fetchJson(
645
+ `${CFS_BASE}/matchItems/round/${roundProviderId}`,
646
+ MatchItemListSchema
647
+ );
648
+ if (!result.success) {
649
+ return result;
650
+ }
651
+ return ok(result.data.items);
652
+ }
653
+ /**
654
+ * Fetch match items for a round by resolving the round provider ID from season and round number.
655
+ *
656
+ * @param seasonId - The compseason ID.
657
+ * @param roundNumber - The round number.
658
+ * @returns Array of match items on success.
659
+ */
660
+ async fetchRoundMatchItemsByNumber(seasonId, roundNumber) {
661
+ const roundsResult = await this.resolveRounds(seasonId);
662
+ if (!roundsResult.success) {
663
+ return roundsResult;
664
+ }
665
+ const round = roundsResult.data.find((r) => r.roundNumber === roundNumber);
666
+ if (!round?.providerId) {
667
+ return err(new AflApiError(`Round not found or missing providerId: round ${roundNumber}`));
668
+ }
669
+ return this.fetchRoundMatchItems(round.providerId);
670
+ }
671
+ /**
672
+ * Fetch match items for all completed rounds in a season.
673
+ *
674
+ * @param seasonId - The compseason ID.
675
+ * @returns Aggregated array of match items from all completed rounds.
676
+ */
677
+ async fetchSeasonMatchItems(seasonId) {
678
+ const roundsResult = await this.resolveRounds(seasonId);
679
+ if (!roundsResult.success) {
680
+ return roundsResult;
681
+ }
682
+ const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
683
+ const results = await Promise.all(providerIds.map((id) => this.fetchRoundMatchItems(id)));
684
+ const allItems = [];
685
+ for (const result of results) {
686
+ if (!result.success) {
687
+ return result;
688
+ }
689
+ const concluded = result.data.filter(
690
+ (item) => item.match.status === "CONCLUDED" || item.match.status === "COMPLETE"
691
+ );
692
+ allItems.push(...concluded);
693
+ }
694
+ return ok(allItems);
695
+ }
696
+ /**
697
+ * Fetch per-player statistics for a match.
698
+ *
699
+ * @param matchProviderId - The match provider ID (e.g. "CD_M20250140101").
700
+ * @returns Player stats list with home and away arrays.
701
+ */
702
+ async fetchPlayerStats(matchProviderId) {
703
+ return this.fetchJson(
704
+ `${CFS_BASE}/playerStats/match/${matchProviderId}`,
705
+ PlayerStatsListSchema
706
+ );
707
+ }
708
+ /**
709
+ * Fetch match roster (lineup) for a match.
710
+ *
711
+ * @param matchProviderId - The match provider ID (e.g. "CD_M20250140101").
712
+ * @returns Match roster with team players.
713
+ */
714
+ async fetchMatchRoster(matchProviderId) {
715
+ return this.fetchJson(`${CFS_BASE}/matchRoster/full/${matchProviderId}`, MatchRosterSchema);
716
+ }
717
+ /**
718
+ * Fetch team list, optionally filtered by team type.
719
+ *
720
+ * @param teamType - Optional filter (e.g. "MEN", "WOMEN").
721
+ * @returns Array of team items.
722
+ */
723
+ async fetchTeams(teamType) {
724
+ const result = await this.fetchJson(`${API_BASE}/teams?pageSize=100`, TeamListSchema);
725
+ if (!result.success) {
726
+ return result;
727
+ }
728
+ if (teamType) {
729
+ return ok(result.data.teams.filter((t) => t.teamType === teamType));
730
+ }
731
+ return ok(result.data.teams);
732
+ }
733
+ /**
734
+ * Fetch squad (roster) for a team in a specific season.
735
+ *
736
+ * @param teamId - The numeric team ID.
737
+ * @param compSeasonId - The compseason ID.
738
+ * @returns Squad list response.
739
+ */
740
+ async fetchSquad(teamId, compSeasonId) {
741
+ return this.fetchJson(
742
+ `${API_BASE}/squads?teamId=${teamId}&compSeasonId=${compSeasonId}`,
743
+ SquadListSchema
744
+ );
745
+ }
746
+ /**
747
+ * Fetch ladder standings for a season (optionally for a specific round).
748
+ *
749
+ * @param seasonId - The compseason ID.
750
+ * @param roundId - Optional round ID (numeric `id`, not `providerId`).
751
+ * @returns Ladder response with entries.
752
+ */
753
+ async fetchLadder(seasonId, roundId) {
754
+ let url = `${API_BASE}/compseasons/${seasonId}/ladders`;
755
+ if (roundId != null) {
756
+ url += `?roundId=${roundId}`;
757
+ }
758
+ return this.fetchJson(url, LadderResponseSchema);
759
+ }
760
+ };
761
+ }
762
+ });
763
+
764
+ // src/transforms/match-results.ts
765
+ function inferRoundType(roundName) {
766
+ return FINALS_PATTERN.test(roundName) ? "Finals" : "HomeAndAway";
767
+ }
768
+ function toMatchStatus(raw) {
769
+ switch (raw) {
770
+ case "CONCLUDED":
771
+ case "COMPLETE":
772
+ return "Complete";
773
+ case "LIVE":
774
+ case "IN_PROGRESS":
775
+ return "Live";
776
+ case "UPCOMING":
777
+ case "SCHEDULED":
778
+ return "Upcoming";
779
+ case "POSTPONED":
780
+ return "Postponed";
781
+ case "CANCELLED":
782
+ return "Cancelled";
783
+ default:
784
+ return "Complete";
785
+ }
786
+ }
787
+ function toQuarterScore(period) {
788
+ return {
789
+ goals: period.score.goals,
790
+ behinds: period.score.behinds,
791
+ points: period.score.totalScore
792
+ };
793
+ }
794
+ function findPeriod(periods, quarter) {
795
+ if (!periods) return null;
796
+ const period = periods.find((p) => p.periodNumber === quarter);
797
+ return period ? toQuarterScore(period) : null;
798
+ }
799
+ function transformMatchItems(items, season, competition, source = "afl-api") {
800
+ return items.map((item) => {
801
+ const homeScore = item.score?.homeTeamScore;
802
+ const awayScore = item.score?.awayTeamScore;
803
+ const homePoints = homeScore?.matchScore.totalScore ?? 0;
804
+ const awayPoints = awayScore?.matchScore.totalScore ?? 0;
805
+ return {
806
+ matchId: item.match.matchId,
807
+ season,
808
+ roundNumber: item.round?.roundNumber ?? 0,
809
+ roundType: inferRoundType(item.round?.name ?? ""),
810
+ date: new Date(item.match.utcStartTime),
811
+ venue: item.venue?.name ?? "",
812
+ homeTeam: normaliseTeamName(item.match.homeTeam.name),
813
+ awayTeam: normaliseTeamName(item.match.awayTeam.name),
814
+ homeGoals: homeScore?.matchScore.goals ?? 0,
815
+ homeBehinds: homeScore?.matchScore.behinds ?? 0,
816
+ homePoints,
817
+ awayGoals: awayScore?.matchScore.goals ?? 0,
818
+ awayBehinds: awayScore?.matchScore.behinds ?? 0,
819
+ awayPoints,
820
+ margin: homePoints - awayPoints,
821
+ q1Home: findPeriod(homeScore?.periodScore, 1),
822
+ q2Home: findPeriod(homeScore?.periodScore, 2),
823
+ q3Home: findPeriod(homeScore?.periodScore, 3),
824
+ q4Home: findPeriod(homeScore?.periodScore, 4),
825
+ q1Away: findPeriod(awayScore?.periodScore, 1),
826
+ q2Away: findPeriod(awayScore?.periodScore, 2),
827
+ q3Away: findPeriod(awayScore?.periodScore, 3),
828
+ q4Away: findPeriod(awayScore?.periodScore, 4),
829
+ status: toMatchStatus(item.match.status),
830
+ attendance: null,
831
+ venueState: item.venue?.state ?? null,
832
+ venueTimezone: item.venue?.timeZone ?? null,
833
+ homeRushedBehinds: homeScore?.rushedBehinds ?? null,
834
+ awayRushedBehinds: awayScore?.rushedBehinds ?? null,
835
+ homeMinutesInFront: homeScore?.minutesInFront ?? null,
836
+ awayMinutesInFront: awayScore?.minutesInFront ?? null,
837
+ source,
838
+ competition
839
+ };
840
+ });
841
+ }
842
+ var FINALS_PATTERN;
843
+ var init_match_results = __esm({
844
+ "src/transforms/match-results.ts"() {
845
+ "use strict";
846
+ init_team_mapping();
847
+ FINALS_PATTERN = /final|elimination|qualifying|preliminary|semi|grand/i;
848
+ }
849
+ });
850
+
851
+ // src/api/fixture.ts
852
+ function toFixture(item, season, fallbackRoundNumber, competition) {
853
+ return {
854
+ matchId: item.match.matchId,
855
+ season,
856
+ roundNumber: item.round?.roundNumber ?? fallbackRoundNumber,
857
+ roundType: inferRoundType(item.round?.name ?? ""),
858
+ date: new Date(item.match.utcStartTime),
859
+ venue: item.venue?.name ?? "",
860
+ homeTeam: normaliseTeamName(item.match.homeTeam.name),
861
+ awayTeam: normaliseTeamName(item.match.awayTeam.name),
862
+ status: toMatchStatus(item.match.status),
863
+ competition
864
+ };
865
+ }
866
+ async function fetchFixture(query) {
867
+ const competition = query.competition ?? "AFLM";
868
+ if (query.source !== "afl-api") {
869
+ return err(
870
+ new UnsupportedSourceError(
871
+ "Fixture data is only available from the AFL API source.",
872
+ query.source
873
+ )
874
+ );
875
+ }
876
+ const client = new AflApiClient();
877
+ const seasonResult = await client.resolveCompSeason(competition, query.season);
878
+ if (!seasonResult.success) return seasonResult;
879
+ if (query.round != null) {
880
+ const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
881
+ if (!itemsResult.success) return itemsResult;
882
+ return ok(itemsResult.data.map((item) => toFixture(item, query.season, 0, competition)));
883
+ }
884
+ const roundsResult = await client.resolveRounds(seasonResult.data);
885
+ if (!roundsResult.success) return roundsResult;
886
+ const roundProviderIds = roundsResult.data.flatMap(
887
+ (r) => r.providerId ? [{ providerId: r.providerId, roundNumber: r.roundNumber }] : []
888
+ );
889
+ const roundResults = await Promise.all(
890
+ roundProviderIds.map((r) => client.fetchRoundMatchItems(r.providerId))
891
+ );
892
+ const fixtures = [];
893
+ for (let i = 0; i < roundResults.length; i++) {
894
+ const result = roundResults[i];
895
+ if (!result?.success) continue;
896
+ const roundNumber = roundProviderIds[i]?.roundNumber ?? 0;
897
+ for (const item of result.data) {
898
+ fixtures.push(toFixture(item, query.season, roundNumber, competition));
899
+ }
900
+ }
901
+ return ok(fixtures);
902
+ }
903
+ var init_fixture = __esm({
904
+ "src/api/fixture.ts"() {
905
+ "use strict";
906
+ init_errors();
907
+ init_result();
908
+ init_team_mapping();
909
+ init_afl_api();
910
+ init_match_results();
911
+ }
912
+ });
913
+
914
+ // src/transforms/ladder.ts
915
+ function transformLadderEntries(entries) {
916
+ return entries.map((entry) => {
917
+ const record = entry.thisSeasonRecord;
918
+ const wl = record?.winLossRecord;
919
+ return {
920
+ position: entry.position,
921
+ team: normaliseTeamName(entry.team.name),
922
+ played: entry.played ?? wl?.played ?? 0,
923
+ wins: wl?.wins ?? 0,
924
+ losses: wl?.losses ?? 0,
925
+ draws: wl?.draws ?? 0,
926
+ pointsFor: entry.pointsFor ?? 0,
927
+ pointsAgainst: entry.pointsAgainst ?? 0,
928
+ percentage: record?.percentage ?? 0,
929
+ premiershipsPoints: record?.aggregatePoints ?? 0,
930
+ form: entry.form ?? null
931
+ };
932
+ });
933
+ }
934
+ var init_ladder = __esm({
935
+ "src/transforms/ladder.ts"() {
936
+ "use strict";
937
+ init_team_mapping();
938
+ }
939
+ });
940
+
941
+ // src/api/ladder.ts
942
+ async function fetchLadder(query) {
943
+ const competition = query.competition ?? "AFLM";
944
+ if (query.source !== "afl-api") {
945
+ return err(
946
+ new UnsupportedSourceError(
947
+ "Ladder data is only available from the AFL API source.",
948
+ query.source
949
+ )
950
+ );
951
+ }
952
+ const client = new AflApiClient();
953
+ const seasonResult = await client.resolveCompSeason(competition, query.season);
954
+ if (!seasonResult.success) return seasonResult;
955
+ let roundId;
956
+ if (query.round != null) {
957
+ const roundsResult = await client.resolveRounds(seasonResult.data);
958
+ if (!roundsResult.success) return roundsResult;
959
+ const round = roundsResult.data.find((r) => r.roundNumber === query.round);
960
+ if (round) {
961
+ roundId = round.id;
962
+ }
17
963
  }
964
+ const ladderResult = await client.fetchLadder(seasonResult.data, roundId);
965
+ if (!ladderResult.success) return ladderResult;
966
+ const firstLadder = ladderResult.data.ladders[0];
967
+ const entries = firstLadder ? transformLadderEntries(firstLadder.entries) : [];
968
+ return ok({
969
+ season: query.season,
970
+ roundNumber: ladderResult.data.round?.roundNumber ?? null,
971
+ entries,
972
+ competition
973
+ });
18
974
  }
975
+ var init_ladder2 = __esm({
976
+ "src/api/ladder.ts"() {
977
+ "use strict";
978
+ init_errors();
979
+ init_result();
980
+ init_afl_api();
981
+ init_ladder();
982
+ }
983
+ });
984
+
985
+ // src/transforms/lineup.ts
986
+ function transformMatchRoster(roster, season, roundNumber, competition) {
987
+ const homeTeamId = roster.match.homeTeamId;
988
+ const awayTeamId = roster.match.awayTeamId;
989
+ const homeTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === homeTeamId);
990
+ const awayTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === awayTeamId);
991
+ const mapPlayers = (players) => players.map((p) => {
992
+ const inner = p.player.player;
993
+ const position = p.player.position ?? null;
994
+ return {
995
+ playerId: inner.playerId,
996
+ givenName: inner.playerName.givenName,
997
+ surname: inner.playerName.surname,
998
+ displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
999
+ jumperNumber: p.jumperNumber ?? null,
1000
+ position,
1001
+ isEmergency: position !== null && EMERGENCY_POSITIONS.has(position),
1002
+ isSubstitute: position !== null && SUBSTITUTE_POSITIONS.has(position)
1003
+ };
1004
+ });
1005
+ return {
1006
+ matchId: roster.match.matchId,
1007
+ season,
1008
+ roundNumber,
1009
+ homeTeam: normaliseTeamName(roster.match.homeTeam.name),
1010
+ awayTeam: normaliseTeamName(roster.match.awayTeam.name),
1011
+ homePlayers: homeTeamPlayers ? mapPlayers(homeTeamPlayers.players) : [],
1012
+ awayPlayers: awayTeamPlayers ? mapPlayers(awayTeamPlayers.players) : [],
1013
+ competition
1014
+ };
1015
+ }
1016
+ var EMERGENCY_POSITIONS, SUBSTITUTE_POSITIONS;
1017
+ var init_lineup = __esm({
1018
+ "src/transforms/lineup.ts"() {
1019
+ "use strict";
1020
+ init_team_mapping();
1021
+ EMERGENCY_POSITIONS = /* @__PURE__ */ new Set(["EMG", "EMERG"]);
1022
+ SUBSTITUTE_POSITIONS = /* @__PURE__ */ new Set(["SUB", "INT"]);
1023
+ }
1024
+ });
19
1025
 
20
- class ScrapeError extends Error {
21
- source;
22
- name = "ScrapeError";
23
- constructor(message, source) {
24
- super(message);
25
- this.source = source;
1026
+ // src/api/lineup.ts
1027
+ async function fetchLineup(query) {
1028
+ const competition = query.competition ?? "AFLM";
1029
+ if (query.source !== "afl-api") {
1030
+ return err(
1031
+ new UnsupportedSourceError(
1032
+ "Lineup data is only available from the AFL API source.",
1033
+ query.source
1034
+ )
1035
+ );
26
1036
  }
1037
+ const client = new AflApiClient();
1038
+ if (query.matchId) {
1039
+ const rosterResult = await client.fetchMatchRoster(query.matchId);
1040
+ if (!rosterResult.success) return rosterResult;
1041
+ return ok([transformMatchRoster(rosterResult.data, query.season, query.round, competition)]);
1042
+ }
1043
+ const seasonResult = await client.resolveCompSeason(competition, query.season);
1044
+ if (!seasonResult.success) return seasonResult;
1045
+ const matchItems = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
1046
+ if (!matchItems.success) return matchItems;
1047
+ if (matchItems.data.length === 0) {
1048
+ return err(new AflApiError(`No matches found for round ${query.round}`));
1049
+ }
1050
+ const rosterResults = await Promise.all(
1051
+ matchItems.data.map((item) => client.fetchMatchRoster(item.match.matchId))
1052
+ );
1053
+ const lineups = [];
1054
+ for (const rosterResult of rosterResults) {
1055
+ if (!rosterResult.success) return rosterResult;
1056
+ lineups.push(transformMatchRoster(rosterResult.data, query.season, query.round, competition));
1057
+ }
1058
+ return ok(lineups);
27
1059
  }
1060
+ var init_lineup2 = __esm({
1061
+ "src/api/lineup.ts"() {
1062
+ "use strict";
1063
+ init_errors();
1064
+ init_result();
1065
+ init_afl_api();
1066
+ init_lineup();
1067
+ }
1068
+ });
28
1069
 
29
- class UnsupportedSourceError extends Error {
30
- source;
31
- name = "UnsupportedSourceError";
32
- constructor(message, source) {
33
- super(message);
34
- this.source = source;
1070
+ // src/lib/date-utils.ts
1071
+ function parseFootyWireDate(dateStr) {
1072
+ const trimmed = dateStr.trim();
1073
+ if (trimmed === "") {
1074
+ return null;
35
1075
  }
1076
+ const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
1077
+ const normalised = withoutDow.replace(/-/g, " ");
1078
+ const match = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
1079
+ if (!match) {
1080
+ return null;
1081
+ }
1082
+ const [, dayStr, monthStr, yearStr] = match;
1083
+ if (!dayStr || !monthStr || !yearStr) {
1084
+ return null;
1085
+ }
1086
+ const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
1087
+ if (monthIndex === void 0) {
1088
+ return null;
1089
+ }
1090
+ const year = Number.parseInt(yearStr, 10);
1091
+ const day = Number.parseInt(dayStr, 10);
1092
+ const date = new Date(Date.UTC(year, monthIndex, day));
1093
+ if (Number.isNaN(date.getTime())) {
1094
+ return null;
1095
+ }
1096
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
1097
+ return null;
1098
+ }
1099
+ return date;
36
1100
  }
1101
+ function parseAflTablesDate(dateStr) {
1102
+ const trimmed = dateStr.trim();
1103
+ if (trimmed === "") {
1104
+ return null;
1105
+ }
1106
+ const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
1107
+ const normalised = withoutDow.replace(/[-/]/g, " ");
1108
+ const dmy = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
1109
+ if (dmy) {
1110
+ const [, dayStr, monthStr, yearStr] = dmy;
1111
+ if (dayStr && monthStr && yearStr) {
1112
+ return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
1113
+ }
1114
+ }
1115
+ const mdy = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
1116
+ if (mdy) {
1117
+ const [, monthStr, dayStr, yearStr] = mdy;
1118
+ if (dayStr && monthStr && yearStr) {
1119
+ return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
1120
+ }
1121
+ }
1122
+ return null;
1123
+ }
1124
+ function buildUtcDate(year, monthStr, day) {
1125
+ const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
1126
+ if (monthIndex === void 0) {
1127
+ return null;
1128
+ }
1129
+ const date = new Date(Date.UTC(year, monthIndex, day));
1130
+ if (Number.isNaN(date.getTime())) {
1131
+ return null;
1132
+ }
1133
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
1134
+ return null;
1135
+ }
1136
+ return date;
1137
+ }
1138
+ var MONTH_ABBREV_TO_INDEX;
1139
+ var init_date_utils = __esm({
1140
+ "src/lib/date-utils.ts"() {
1141
+ "use strict";
1142
+ MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
1143
+ ["jan", 0],
1144
+ ["feb", 1],
1145
+ ["mar", 2],
1146
+ ["apr", 3],
1147
+ ["may", 4],
1148
+ ["jun", 5],
1149
+ ["jul", 6],
1150
+ ["aug", 7],
1151
+ ["sep", 8],
1152
+ ["oct", 9],
1153
+ ["nov", 10],
1154
+ ["dec", 11],
1155
+ ["january", 0],
1156
+ ["february", 1],
1157
+ ["march", 2],
1158
+ ["april", 3],
1159
+ ["june", 5],
1160
+ ["july", 6],
1161
+ ["august", 7],
1162
+ ["september", 8],
1163
+ ["october", 9],
1164
+ ["november", 10],
1165
+ ["december", 11]
1166
+ ]);
1167
+ }
1168
+ });
37
1169
 
38
- class ValidationError extends Error {
39
- issues;
40
- name = "ValidationError";
41
- constructor(message, issues) {
42
- super(message);
43
- this.issues = issues;
1170
+ // src/sources/afl-tables.ts
1171
+ import * as cheerio from "cheerio";
1172
+ function parseSeasonPage(html, year) {
1173
+ const $ = cheerio.load(html);
1174
+ const results = [];
1175
+ let currentRound = 0;
1176
+ let currentRoundType = "HomeAndAway";
1177
+ let matchCounter = 0;
1178
+ $("table").each((_i, table) => {
1179
+ const $table = $(table);
1180
+ const text = $table.text().trim();
1181
+ const roundMatch = /^Round\s+(\d+)/i.exec(text);
1182
+ if (roundMatch?.[1] && !$table.attr("border")) {
1183
+ currentRound = Number.parseInt(roundMatch[1], 10);
1184
+ currentRoundType = inferRoundType(text);
1185
+ return;
1186
+ }
1187
+ if (!$table.attr("border") && inferRoundType(text) === "Finals") {
1188
+ currentRoundType = "Finals";
1189
+ return;
1190
+ }
1191
+ if ($table.attr("border") !== "1") return;
1192
+ const rows = $table.find("tr");
1193
+ if (rows.length !== 2) return;
1194
+ const homeRow = $(rows[0]);
1195
+ const awayRow = $(rows[1]);
1196
+ const homeCells = homeRow.find("td");
1197
+ const awayCells = awayRow.find("td");
1198
+ if (homeCells.length < 4 || awayCells.length < 3) return;
1199
+ const homeTeam = normaliseTeamName($(homeCells[0]).find("a").text().trim());
1200
+ const awayTeam = normaliseTeamName($(awayCells[0]).find("a").text().trim());
1201
+ if (!homeTeam || !awayTeam) return;
1202
+ const homeQuarters = parseQuarterScores($(homeCells[1]).text());
1203
+ const awayQuarters = parseQuarterScores($(awayCells[1]).text());
1204
+ const homePoints = Number.parseInt($(homeCells[2]).text().trim(), 10) || 0;
1205
+ const awayPoints = Number.parseInt($(awayCells[2]).text().trim(), 10) || 0;
1206
+ const infoText = $(homeCells[3]).text().trim();
1207
+ const date = parseDateFromInfo(infoText, year);
1208
+ const venue = parseVenueFromInfo($(homeCells[3]).html() ?? "");
1209
+ const attendance = parseAttendanceFromInfo(infoText);
1210
+ const homeFinal = homeQuarters[3];
1211
+ const awayFinal = awayQuarters[3];
1212
+ matchCounter++;
1213
+ results.push({
1214
+ matchId: `AT_${year}_${matchCounter}`,
1215
+ season: year,
1216
+ roundNumber: currentRound,
1217
+ roundType: currentRoundType,
1218
+ date,
1219
+ venue,
1220
+ homeTeam,
1221
+ awayTeam,
1222
+ homeGoals: homeFinal?.goals ?? 0,
1223
+ homeBehinds: homeFinal?.behinds ?? 0,
1224
+ homePoints,
1225
+ awayGoals: awayFinal?.goals ?? 0,
1226
+ awayBehinds: awayFinal?.behinds ?? 0,
1227
+ awayPoints,
1228
+ margin: homePoints - awayPoints,
1229
+ q1Home: homeQuarters[0] ?? null,
1230
+ q2Home: homeQuarters[1] ?? null,
1231
+ q3Home: homeQuarters[2] ?? null,
1232
+ q4Home: homeQuarters[3] ?? null,
1233
+ q1Away: awayQuarters[0] ?? null,
1234
+ q2Away: awayQuarters[1] ?? null,
1235
+ q3Away: awayQuarters[2] ?? null,
1236
+ q4Away: awayQuarters[3] ?? null,
1237
+ status: "Complete",
1238
+ attendance,
1239
+ venueState: null,
1240
+ venueTimezone: null,
1241
+ homeRushedBehinds: null,
1242
+ awayRushedBehinds: null,
1243
+ homeMinutesInFront: null,
1244
+ awayMinutesInFront: null,
1245
+ source: "afl-tables",
1246
+ competition: "AFLM"
1247
+ });
1248
+ });
1249
+ return results;
1250
+ }
1251
+ function parseQuarterScores(text) {
1252
+ const clean = text.replace(/\u00a0/g, " ").trim();
1253
+ const matches = [...clean.matchAll(/(\d+)\.(\d+)/g)];
1254
+ return matches.map((m) => {
1255
+ const goals = Number.parseInt(m[1] ?? "0", 10);
1256
+ const behinds = Number.parseInt(m[2] ?? "0", 10);
1257
+ return { goals, behinds, points: goals * 6 + behinds };
1258
+ });
1259
+ }
1260
+ function parseDateFromInfo(text, year) {
1261
+ const dateMatch = /(\d{1,2}-[A-Z][a-z]{2}-\d{4})/.exec(text);
1262
+ if (dateMatch?.[1]) {
1263
+ return parseAflTablesDate(dateMatch[1]) ?? new Date(year, 0, 1);
44
1264
  }
1265
+ return parseAflTablesDate(text) ?? new Date(year, 0, 1);
45
1266
  }
1267
+ function parseVenueFromInfo(html) {
1268
+ const $ = cheerio.load(html);
1269
+ const venueLink = $("a[href*='venues']");
1270
+ if (venueLink.length > 0) {
1271
+ return venueLink.text().trim();
1272
+ }
1273
+ const venueMatch = /Venue:\s*(.+?)(?:<|$)/i.exec(html);
1274
+ return venueMatch?.[1]?.trim() ?? "";
1275
+ }
1276
+ function parseAttendanceFromInfo(text) {
1277
+ const match = /Att:\s*([\d,]+)/i.exec(text);
1278
+ if (!match?.[1]) return null;
1279
+ return Number.parseInt(match[1].replace(/,/g, ""), 10) || null;
1280
+ }
1281
+ var AFL_TABLES_BASE, AflTablesClient;
1282
+ var init_afl_tables = __esm({
1283
+ "src/sources/afl-tables.ts"() {
1284
+ "use strict";
1285
+ init_date_utils();
1286
+ init_errors();
1287
+ init_result();
1288
+ init_team_mapping();
1289
+ init_match_results();
1290
+ AFL_TABLES_BASE = "https://afltables.com/afl/seas";
1291
+ AflTablesClient = class {
1292
+ fetchFn;
1293
+ constructor(options) {
1294
+ this.fetchFn = options?.fetchFn ?? globalThis.fetch;
1295
+ }
1296
+ /**
1297
+ * Fetch season match results from AFL Tables.
1298
+ *
1299
+ * @param year - The season year (1897 to present).
1300
+ * @returns Array of match results.
1301
+ */
1302
+ async fetchSeasonResults(year) {
1303
+ const url = `${AFL_TABLES_BASE}/${year}.html`;
1304
+ try {
1305
+ const response = await this.fetchFn(url, {
1306
+ headers: { "User-Agent": "Mozilla/5.0" }
1307
+ });
1308
+ if (!response.ok) {
1309
+ return err(
1310
+ new ScrapeError(`AFL Tables request failed: ${response.status} (${url})`, "afl-tables")
1311
+ );
1312
+ }
1313
+ const html = await response.text();
1314
+ const results = parseSeasonPage(html, year);
1315
+ return ok(results);
1316
+ } catch (cause) {
1317
+ return err(
1318
+ new ScrapeError(
1319
+ `AFL Tables request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
1320
+ "afl-tables"
1321
+ )
1322
+ );
1323
+ }
1324
+ }
1325
+ };
1326
+ }
1327
+ });
1328
+
1329
+ // src/sources/footywire.ts
1330
+ import * as cheerio2 from "cheerio";
1331
+ function parseMatchList(html, year) {
1332
+ const $ = cheerio2.load(html);
1333
+ const results = [];
1334
+ let currentRound = 0;
1335
+ let currentRoundType = "HomeAndAway";
1336
+ $("tr").each((_i, row) => {
1337
+ const roundHeader = $(row).find("td[colspan='7']");
1338
+ if (roundHeader.length > 0) {
1339
+ const text = roundHeader.text().trim();
1340
+ currentRoundType = inferRoundType(text);
1341
+ const roundMatch = /Round\s+(\d+)/i.exec(text);
1342
+ if (roundMatch?.[1]) {
1343
+ currentRound = Number.parseInt(roundMatch[1], 10);
1344
+ }
1345
+ return;
1346
+ }
1347
+ const cells = $(row).find("td.data");
1348
+ if (cells.length < 5) return;
1349
+ const dateText = $(cells[0]).text().trim();
1350
+ const teamsCell = $(cells[1]);
1351
+ const venue = $(cells[2]).text().trim();
1352
+ const attendance = $(cells[3]).text().trim();
1353
+ const scoreCell = $(cells[4]);
1354
+ if (venue === "BYE") return;
1355
+ const teamLinks = teamsCell.find("a");
1356
+ if (teamLinks.length < 2) return;
1357
+ const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
1358
+ const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
1359
+ const scoreText = scoreCell.text().trim();
1360
+ const scoreMatch = /(\d+)-(\d+)/.exec(scoreText);
1361
+ if (!scoreMatch) return;
1362
+ const homePoints = Number.parseInt(scoreMatch[1] ?? "0", 10);
1363
+ const awayPoints = Number.parseInt(scoreMatch[2] ?? "0", 10);
1364
+ const scoreLink = scoreCell.find("a").attr("href") ?? "";
1365
+ const midMatch = /mid=(\d+)/.exec(scoreLink);
1366
+ const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
1367
+ const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
1368
+ const homeGoals = Math.floor(homePoints / 6);
1369
+ const homeBehinds = homePoints - homeGoals * 6;
1370
+ const awayGoals = Math.floor(awayPoints / 6);
1371
+ const awayBehinds = awayPoints - awayGoals * 6;
1372
+ results.push({
1373
+ matchId,
1374
+ season: year,
1375
+ roundNumber: currentRound,
1376
+ roundType: currentRoundType,
1377
+ date,
1378
+ venue,
1379
+ homeTeam,
1380
+ awayTeam,
1381
+ homeGoals,
1382
+ homeBehinds,
1383
+ homePoints,
1384
+ awayGoals,
1385
+ awayBehinds,
1386
+ awayPoints,
1387
+ margin: homePoints - awayPoints,
1388
+ q1Home: null,
1389
+ q2Home: null,
1390
+ q3Home: null,
1391
+ q4Home: null,
1392
+ q1Away: null,
1393
+ q2Away: null,
1394
+ q3Away: null,
1395
+ q4Away: null,
1396
+ status: "Complete",
1397
+ attendance: attendance ? Number.parseInt(attendance, 10) || null : null,
1398
+ venueState: null,
1399
+ venueTimezone: null,
1400
+ homeRushedBehinds: null,
1401
+ awayRushedBehinds: null,
1402
+ homeMinutesInFront: null,
1403
+ awayMinutesInFront: null,
1404
+ source: "footywire",
1405
+ competition: "AFLM"
1406
+ });
1407
+ });
1408
+ return results;
1409
+ }
1410
+ var FOOTYWIRE_BASE, FootyWireClient;
1411
+ var init_footywire = __esm({
1412
+ "src/sources/footywire.ts"() {
1413
+ "use strict";
1414
+ init_date_utils();
1415
+ init_errors();
1416
+ init_result();
1417
+ init_team_mapping();
1418
+ init_match_results();
1419
+ FOOTYWIRE_BASE = "https://www.footywire.com/afl/footy";
1420
+ FootyWireClient = class {
1421
+ fetchFn;
1422
+ constructor(options) {
1423
+ this.fetchFn = options?.fetchFn ?? globalThis.fetch;
1424
+ }
1425
+ /**
1426
+ * Fetch the HTML content of a FootyWire page.
1427
+ */
1428
+ async fetchHtml(url) {
1429
+ try {
1430
+ const response = await this.fetchFn(url, {
1431
+ headers: {
1432
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
1433
+ }
1434
+ });
1435
+ if (!response.ok) {
1436
+ return err(
1437
+ new ScrapeError(`FootyWire request failed: ${response.status} (${url})`, "footywire")
1438
+ );
1439
+ }
1440
+ const html = await response.text();
1441
+ return ok(html);
1442
+ } catch (cause) {
1443
+ return err(
1444
+ new ScrapeError(
1445
+ `FootyWire request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
1446
+ "footywire"
1447
+ )
1448
+ );
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Fetch season match results from FootyWire.
1453
+ *
1454
+ * @param year - The season year.
1455
+ * @returns Array of match results.
1456
+ */
1457
+ async fetchSeasonResults(year) {
1458
+ const url = `${FOOTYWIRE_BASE}/ft_match_list?year=${year}`;
1459
+ const htmlResult = await this.fetchHtml(url);
1460
+ if (!htmlResult.success) {
1461
+ return htmlResult;
1462
+ }
1463
+ try {
1464
+ const results = parseMatchList(htmlResult.data, year);
1465
+ return ok(results);
1466
+ } catch (cause) {
1467
+ return err(
1468
+ new ScrapeError(
1469
+ `Failed to parse match list: ${cause instanceof Error ? cause.message : String(cause)}`,
1470
+ "footywire"
1471
+ )
1472
+ );
1473
+ }
1474
+ }
1475
+ };
1476
+ }
1477
+ });
1478
+
1479
+ // src/api/match-results.ts
1480
+ async function fetchMatchResults(query) {
1481
+ const competition = query.competition ?? "AFLM";
1482
+ switch (query.source) {
1483
+ case "afl-api": {
1484
+ const client = new AflApiClient();
1485
+ const seasonResult = await client.resolveCompSeason(competition, query.season);
1486
+ if (!seasonResult.success) return seasonResult;
1487
+ if (query.round != null) {
1488
+ const itemsResult2 = await client.fetchRoundMatchItemsByNumber(
1489
+ seasonResult.data,
1490
+ query.round
1491
+ );
1492
+ if (!itemsResult2.success) return itemsResult2;
1493
+ return ok(transformMatchItems(itemsResult2.data, query.season, competition));
1494
+ }
1495
+ const itemsResult = await client.fetchSeasonMatchItems(seasonResult.data);
1496
+ if (!itemsResult.success) return itemsResult;
1497
+ return ok(transformMatchItems(itemsResult.data, query.season, competition));
1498
+ }
1499
+ case "footywire": {
1500
+ const client = new FootyWireClient();
1501
+ const result = await client.fetchSeasonResults(query.season);
1502
+ if (!result.success) return result;
1503
+ if (query.round != null) {
1504
+ return ok(result.data.filter((m) => m.roundNumber === query.round));
1505
+ }
1506
+ return result;
1507
+ }
1508
+ case "afl-tables": {
1509
+ const client = new AflTablesClient();
1510
+ const result = await client.fetchSeasonResults(query.season);
1511
+ if (!result.success) return result;
1512
+ if (query.round != null) {
1513
+ return ok(result.data.filter((m) => m.roundNumber === query.round));
1514
+ }
1515
+ return result;
1516
+ }
1517
+ default:
1518
+ return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
1519
+ }
1520
+ }
1521
+ var init_match_results2 = __esm({
1522
+ "src/api/match-results.ts"() {
1523
+ "use strict";
1524
+ init_errors();
1525
+ init_result();
1526
+ init_afl_api();
1527
+ init_afl_tables();
1528
+ init_footywire();
1529
+ init_match_results();
1530
+ }
1531
+ });
1532
+
1533
+ // src/transforms/player-stats.ts
1534
+ function toNullable(value) {
1535
+ return value ?? null;
1536
+ }
1537
+ function transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap) {
1538
+ const inner = item.player.player.player;
1539
+ const stats = item.playerStats.stats;
1540
+ const clearances = stats.clearances;
1541
+ return {
1542
+ matchId,
1543
+ season,
1544
+ roundNumber,
1545
+ team: normaliseTeamName(teamIdMap?.get(item.teamId) ?? item.teamId),
1546
+ competition,
1547
+ playerId: inner.playerId,
1548
+ givenName: inner.playerName.givenName,
1549
+ surname: inner.playerName.surname,
1550
+ displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
1551
+ jumperNumber: item.player.jumperNumber ?? null,
1552
+ kicks: toNullable(stats.kicks),
1553
+ handballs: toNullable(stats.handballs),
1554
+ disposals: toNullable(stats.disposals),
1555
+ marks: toNullable(stats.marks),
1556
+ goals: toNullable(stats.goals),
1557
+ behinds: toNullable(stats.behinds),
1558
+ tackles: toNullable(stats.tackles),
1559
+ hitouts: toNullable(stats.hitouts),
1560
+ freesFor: toNullable(stats.freesFor),
1561
+ freesAgainst: toNullable(stats.freesAgainst),
1562
+ contestedPossessions: toNullable(stats.contestedPossessions),
1563
+ uncontestedPossessions: toNullable(stats.uncontestedPossessions),
1564
+ contestedMarks: toNullable(stats.contestedMarks),
1565
+ intercepts: toNullable(stats.intercepts),
1566
+ centreClearances: toNullable(clearances?.centreClearances),
1567
+ stoppageClearances: toNullable(clearances?.stoppageClearances),
1568
+ totalClearances: toNullable(clearances?.totalClearances),
1569
+ inside50s: toNullable(stats.inside50s),
1570
+ rebound50s: toNullable(stats.rebound50s),
1571
+ clangers: toNullable(stats.clangers),
1572
+ turnovers: toNullable(stats.turnovers),
1573
+ onePercenters: toNullable(stats.onePercenters),
1574
+ bounces: toNullable(stats.bounces),
1575
+ goalAssists: toNullable(stats.goalAssists),
1576
+ disposalEfficiency: toNullable(stats.disposalEfficiency),
1577
+ metresGained: toNullable(stats.metresGained),
1578
+ goalAccuracy: toNullable(stats.goalAccuracy),
1579
+ marksInside50: toNullable(stats.marksInside50),
1580
+ tacklesInside50: toNullable(stats.tacklesInside50),
1581
+ shotsAtGoal: toNullable(stats.shotsAtGoal),
1582
+ scoreInvolvements: toNullable(stats.scoreInvolvements),
1583
+ totalPossessions: toNullable(stats.totalPossessions),
1584
+ timeOnGroundPercentage: toNullable(item.playerStats.timeOnGroundPercentage),
1585
+ ratingPoints: toNullable(stats.ratingPoints),
1586
+ dreamTeamPoints: toNullable(stats.dreamTeamPoints),
1587
+ effectiveDisposals: toNullable(stats.extendedStats?.effectiveDisposals),
1588
+ effectiveKicks: toNullable(stats.extendedStats?.effectiveKicks),
1589
+ kickEfficiency: toNullable(stats.extendedStats?.kickEfficiency),
1590
+ kickToHandballRatio: toNullable(stats.extendedStats?.kickToHandballRatio),
1591
+ pressureActs: toNullable(stats.extendedStats?.pressureActs),
1592
+ defHalfPressureActs: toNullable(stats.extendedStats?.defHalfPressureActs),
1593
+ spoils: toNullable(stats.extendedStats?.spoils),
1594
+ hitoutsToAdvantage: toNullable(stats.extendedStats?.hitoutsToAdvantage),
1595
+ hitoutWinPercentage: toNullable(stats.extendedStats?.hitoutWinPercentage),
1596
+ hitoutToAdvantageRate: toNullable(stats.extendedStats?.hitoutToAdvantageRate),
1597
+ groundBallGets: toNullable(stats.extendedStats?.groundBallGets),
1598
+ f50GroundBallGets: toNullable(stats.extendedStats?.f50GroundBallGets),
1599
+ interceptMarks: toNullable(stats.extendedStats?.interceptMarks),
1600
+ marksOnLead: toNullable(stats.extendedStats?.marksOnLead),
1601
+ contestedPossessionRate: toNullable(stats.extendedStats?.contestedPossessionRate),
1602
+ contestOffOneOnOnes: toNullable(stats.extendedStats?.contestOffOneOnOnes),
1603
+ contestOffWins: toNullable(stats.extendedStats?.contestOffWins),
1604
+ contestOffWinsPercentage: toNullable(stats.extendedStats?.contestOffWinsPercentage),
1605
+ contestDefOneOnOnes: toNullable(stats.extendedStats?.contestDefOneOnOnes),
1606
+ contestDefLosses: toNullable(stats.extendedStats?.contestDefLosses),
1607
+ contestDefLossPercentage: toNullable(stats.extendedStats?.contestDefLossPercentage),
1608
+ centreBounceAttendances: toNullable(stats.extendedStats?.centreBounceAttendances),
1609
+ kickins: toNullable(stats.extendedStats?.kickins),
1610
+ kickinsPlayon: toNullable(stats.extendedStats?.kickinsPlayon),
1611
+ ruckContests: toNullable(stats.extendedStats?.ruckContests),
1612
+ scoreLaunches: toNullable(stats.extendedStats?.scoreLaunches),
1613
+ source
1614
+ };
1615
+ }
1616
+ function transformPlayerStats(data, matchId, season, roundNumber, competition, source = "afl-api", teamIdMap) {
1617
+ const home = data.homeTeamPlayerStats.map(
1618
+ (item) => transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap)
1619
+ );
1620
+ const away = data.awayTeamPlayerStats.map(
1621
+ (item) => transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap)
1622
+ );
1623
+ return [...home, ...away];
1624
+ }
1625
+ var init_player_stats = __esm({
1626
+ "src/transforms/player-stats.ts"() {
1627
+ "use strict";
1628
+ init_team_mapping();
1629
+ }
1630
+ });
1631
+
1632
+ // src/api/player-stats.ts
1633
+ async function fetchPlayerStats(query) {
1634
+ const competition = query.competition ?? "AFLM";
1635
+ switch (query.source) {
1636
+ case "afl-api": {
1637
+ const client = new AflApiClient();
1638
+ if (query.matchId) {
1639
+ const result = await client.fetchPlayerStats(query.matchId);
1640
+ if (!result.success) return result;
1641
+ return ok(
1642
+ transformPlayerStats(
1643
+ result.data,
1644
+ query.matchId,
1645
+ query.season,
1646
+ query.round ?? 0,
1647
+ competition
1648
+ )
1649
+ );
1650
+ }
1651
+ const seasonResult = await client.resolveCompSeason(competition, query.season);
1652
+ if (!seasonResult.success) return seasonResult;
1653
+ const roundNumber = query.round ?? 1;
1654
+ const matchItemsResult = await client.fetchRoundMatchItemsByNumber(
1655
+ seasonResult.data,
1656
+ roundNumber
1657
+ );
1658
+ if (!matchItemsResult.success) return matchItemsResult;
1659
+ const teamIdMap = /* @__PURE__ */ new Map();
1660
+ for (const item of matchItemsResult.data) {
1661
+ teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
1662
+ teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
1663
+ }
1664
+ const statsResults = await Promise.all(
1665
+ matchItemsResult.data.map((item) => client.fetchPlayerStats(item.match.matchId))
1666
+ );
1667
+ const allStats = [];
1668
+ for (let i = 0; i < statsResults.length; i++) {
1669
+ const statsResult = statsResults[i];
1670
+ if (!statsResult?.success)
1671
+ return statsResult ?? err(new AflApiError("Missing stats result"));
1672
+ const item = matchItemsResult.data[i];
1673
+ if (!item) continue;
1674
+ allStats.push(
1675
+ ...transformPlayerStats(
1676
+ statsResult.data,
1677
+ item.match.matchId,
1678
+ query.season,
1679
+ roundNumber,
1680
+ competition,
1681
+ "afl-api",
1682
+ teamIdMap
1683
+ )
1684
+ );
1685
+ }
1686
+ return ok(allStats);
1687
+ }
1688
+ case "footywire":
1689
+ return err(
1690
+ new UnsupportedSourceError(
1691
+ "Player stats from FootyWire are not yet supported. Use source: 'afl-api'.",
1692
+ "footywire"
1693
+ )
1694
+ );
1695
+ case "afl-tables":
1696
+ return err(
1697
+ new UnsupportedSourceError(
1698
+ "Player stats from AFL Tables are not yet supported. Use source: 'afl-api'.",
1699
+ "afl-tables"
1700
+ )
1701
+ );
1702
+ default:
1703
+ return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
1704
+ }
1705
+ }
1706
+ var init_player_stats2 = __esm({
1707
+ "src/api/player-stats.ts"() {
1708
+ "use strict";
1709
+ init_errors();
1710
+ init_result();
1711
+ init_afl_api();
1712
+ init_player_stats();
1713
+ }
1714
+ });
1715
+
1716
+ // src/api/teams.ts
1717
+ function teamTypeForComp(comp) {
1718
+ return comp === "AFLW" ? "WOMEN" : "MEN";
1719
+ }
1720
+ async function fetchTeams(query) {
1721
+ const client = new AflApiClient();
1722
+ const teamType = query?.teamType ?? (query?.competition ? teamTypeForComp(query.competition) : void 0);
1723
+ const result = await client.fetchTeams(teamType);
1724
+ if (!result.success) return result;
1725
+ const competition = query?.competition ?? "AFLM";
1726
+ return ok(
1727
+ result.data.map((t) => ({
1728
+ teamId: String(t.id),
1729
+ name: normaliseTeamName(t.name),
1730
+ abbreviation: t.abbreviation ?? "",
1731
+ competition
1732
+ }))
1733
+ );
1734
+ }
1735
+ async function fetchSquad(query) {
1736
+ const client = new AflApiClient();
1737
+ const competition = query.competition ?? "AFLM";
1738
+ const seasonResult = await client.resolveCompSeason(competition, query.season);
1739
+ if (!seasonResult.success) return seasonResult;
1740
+ const teamId = Number.parseInt(query.teamId, 10);
1741
+ if (Number.isNaN(teamId)) {
1742
+ return err(new ValidationError(`Invalid team ID: ${query.teamId}`));
1743
+ }
1744
+ const squadResult = await client.fetchSquad(teamId, seasonResult.data);
1745
+ if (!squadResult.success) return squadResult;
1746
+ const players = squadResult.data.squad.players.map((p) => ({
1747
+ playerId: p.player.providerId ?? String(p.player.id),
1748
+ givenName: p.player.firstName,
1749
+ surname: p.player.surname,
1750
+ displayName: `${p.player.firstName} ${p.player.surname}`,
1751
+ jumperNumber: p.jumperNumber ?? null,
1752
+ position: p.position ?? null,
1753
+ dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
1754
+ heightCm: p.player.heightInCm ?? null,
1755
+ weightKg: p.player.weightInKg ?? null,
1756
+ draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
1757
+ draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
1758
+ draftType: p.player.draftType ?? null,
1759
+ debutYear: p.player.debutYear ? Number.parseInt(p.player.debutYear, 10) || null : null,
1760
+ recruitedFrom: p.player.recruitedFrom ?? null
1761
+ }));
1762
+ return ok({
1763
+ teamId: query.teamId,
1764
+ teamName: normaliseTeamName(squadResult.data.squad.team?.name ?? query.teamId),
1765
+ season: query.season,
1766
+ players,
1767
+ competition
1768
+ });
1769
+ }
1770
+ var init_teams = __esm({
1771
+ "src/api/teams.ts"() {
1772
+ "use strict";
1773
+ init_errors();
1774
+ init_result();
1775
+ init_team_mapping();
1776
+ init_afl_api();
1777
+ }
1778
+ });
1779
+
1780
+ // src/index.ts
1781
+ var init_index = __esm({
1782
+ "src/index.ts"() {
1783
+ "use strict";
1784
+ init_fixture();
1785
+ init_ladder2();
1786
+ init_lineup2();
1787
+ init_match_results2();
1788
+ init_player_stats2();
1789
+ init_teams();
1790
+ }
1791
+ });
1792
+
1793
+ // src/cli/formatters/csv.ts
1794
+ function escapeField(value) {
1795
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1796
+ return `"${value.replace(/"/g, '""')}"`;
1797
+ }
1798
+ return value;
1799
+ }
1800
+ function toStringValue(value) {
1801
+ if (value === null || value === void 0) return "";
1802
+ if (value instanceof Date) return value.toISOString();
1803
+ if (typeof value === "object") return JSON.stringify(value);
1804
+ return String(value);
1805
+ }
1806
+ function formatCsv(data) {
1807
+ if (data.length === 0) return "";
1808
+ const firstRow = data[0];
1809
+ if (!firstRow) return "";
1810
+ const headers = Object.keys(firstRow);
1811
+ const lines = [headers.map(escapeField).join(",")];
1812
+ for (const row of data) {
1813
+ const values = headers.map((h) => escapeField(toStringValue(row[h])));
1814
+ lines.push(values.join(","));
1815
+ }
1816
+ return lines.join("\n");
1817
+ }
1818
+ var init_csv = __esm({
1819
+ "src/cli/formatters/csv.ts"() {
1820
+ "use strict";
1821
+ }
1822
+ });
1823
+
1824
+ // src/cli/formatters/json.ts
1825
+ function formatJson(data) {
1826
+ return JSON.stringify(data, null, 2);
1827
+ }
1828
+ var init_json = __esm({
1829
+ "src/cli/formatters/json.ts"() {
1830
+ "use strict";
1831
+ }
1832
+ });
1833
+
1834
+ // src/cli/formatters/table.ts
1835
+ function toDisplayValue(value) {
1836
+ if (value === null || value === void 0) return "-";
1837
+ if (value instanceof Date) return value.toISOString().slice(0, 16).replace("T", " ");
1838
+ if (typeof value === "object") return JSON.stringify(value);
1839
+ return String(value);
1840
+ }
1841
+ function truncate(str, maxLen) {
1842
+ if (str.length <= maxLen) return str;
1843
+ return `${str.slice(0, maxLen - 1)}\u2026`;
1844
+ }
1845
+ function formatTable(data, options = {}) {
1846
+ if (data.length === 0) return "No data.";
1847
+ const firstRow = data[0];
1848
+ if (!firstRow) return "No data.";
1849
+ const termWidth = options.terminalWidth ?? process.stdout.columns ?? 120;
1850
+ const allKeys = Object.keys(firstRow);
1851
+ let columns;
1852
+ if (options.full || !options.columns || options.columns.length === 0) {
1853
+ columns = allKeys.map((key) => ({ key }));
1854
+ } else {
1855
+ columns = [...options.columns];
1856
+ }
1857
+ const colWidths = columns.map((col) => (col.label ?? col.key).length);
1858
+ for (const row of data) {
1859
+ for (let i = 0; i < columns.length; i++) {
1860
+ const col = columns[i];
1861
+ if (!col) continue;
1862
+ const len = toDisplayValue(row[col.key]).length;
1863
+ const current = colWidths[i];
1864
+ if (current !== void 0 && len > current) {
1865
+ colWidths[i] = len;
1866
+ }
1867
+ }
1868
+ }
1869
+ for (let i = 0; i < columns.length; i++) {
1870
+ const col = columns[i];
1871
+ const width = colWidths[i];
1872
+ if (!col || width === void 0) continue;
1873
+ colWidths[i] = Math.min(col.maxWidth ?? 30, width);
1874
+ }
1875
+ const gap = 2;
1876
+ const visibleCols = [];
1877
+ let usedWidth = 0;
1878
+ for (let i = 0; i < columns.length; i++) {
1879
+ const colWidth = colWidths[i];
1880
+ if (colWidth === void 0) continue;
1881
+ const needed = usedWidth > 0 ? colWidth + gap : colWidth;
1882
+ if (usedWidth + needed > termWidth && visibleCols.length > 0) break;
1883
+ visibleCols.push(i);
1884
+ usedWidth += needed;
1885
+ }
1886
+ const headerParts = visibleCols.map((i) => {
1887
+ const col = columns[i];
1888
+ const width = colWidths[i];
1889
+ if (!col || width === void 0) return "";
1890
+ const label = (col.label ?? col.key).toUpperCase();
1891
+ return truncate(label, width).padEnd(width);
1892
+ });
1893
+ const header = headerParts.join(" ");
1894
+ const separator = visibleCols.map((i) => {
1895
+ const width = colWidths[i];
1896
+ if (width === void 0) return "";
1897
+ return "\u2500".repeat(width);
1898
+ }).join(" ");
1899
+ const rows = data.map((row) => {
1900
+ const parts = visibleCols.map((i) => {
1901
+ const col = columns[i];
1902
+ const width = colWidths[i];
1903
+ if (!col || width === void 0) return "";
1904
+ const val = toDisplayValue(row[col.key]);
1905
+ return truncate(val, width).padEnd(width);
1906
+ });
1907
+ return parts.join(" ");
1908
+ });
1909
+ return [header, separator, ...rows].join("\n");
1910
+ }
1911
+ var init_table = __esm({
1912
+ "src/cli/formatters/table.ts"() {
1913
+ "use strict";
1914
+ }
1915
+ });
1916
+
1917
+ // src/cli/formatters/index.ts
1918
+ function resolveFormat(options) {
1919
+ if (options.json) return "json";
1920
+ if (options.csv) return "csv";
1921
+ if (options.format === "json" || options.format === "csv" || options.format === "table") {
1922
+ return options.format;
1923
+ }
1924
+ if (!process.stdout.isTTY) return "json";
1925
+ return "table";
1926
+ }
1927
+ function formatOutput(data, options) {
1928
+ const rows = data;
1929
+ const format = resolveFormat(options);
1930
+ switch (format) {
1931
+ case "json":
1932
+ return formatJson(rows);
1933
+ case "csv":
1934
+ return formatCsv(rows);
1935
+ case "table":
1936
+ return formatTable(rows, {
1937
+ columns: options.columns,
1938
+ full: options.full ?? false
1939
+ });
1940
+ }
1941
+ }
1942
+ var init_formatters = __esm({
1943
+ "src/cli/formatters/index.ts"() {
1944
+ "use strict";
1945
+ init_csv();
1946
+ init_json();
1947
+ init_table();
1948
+ }
1949
+ });
1950
+
1951
+ // src/cli/ui.ts
1952
+ import { spinner } from "@clack/prompts";
1953
+ import pc from "picocolors";
1954
+ async function withSpinner(message, fn) {
1955
+ if (!isTTY) {
1956
+ return fn();
1957
+ }
1958
+ const s = spinner();
1959
+ s.start(message);
1960
+ try {
1961
+ const result = await fn();
1962
+ s.stop(message);
1963
+ return result;
1964
+ } catch (error) {
1965
+ s.error("Failed");
1966
+ throw error;
1967
+ }
1968
+ }
1969
+ function showSummary(message) {
1970
+ if (!isTTY) return;
1971
+ console.error(pc.dim(message));
1972
+ }
1973
+ var isTTY;
1974
+ var init_ui = __esm({
1975
+ "src/cli/ui.ts"() {
1976
+ "use strict";
1977
+ isTTY = process.stdout.isTTY === true;
1978
+ }
1979
+ });
1980
+
1981
+ // src/cli/commands/matches.ts
1982
+ var matches_exports = {};
1983
+ __export(matches_exports, {
1984
+ matchesCommand: () => matchesCommand
1985
+ });
1986
+ import { defineCommand } from "citty";
1987
+ var DEFAULT_COLUMNS, matchesCommand;
1988
+ var init_matches = __esm({
1989
+ "src/cli/commands/matches.ts"() {
1990
+ "use strict";
1991
+ init_index();
1992
+ init_formatters();
1993
+ init_ui();
1994
+ DEFAULT_COLUMNS = [
1995
+ { key: "date", label: "Date", maxWidth: 16 },
1996
+ { key: "roundNumber", label: "Round", maxWidth: 6 },
1997
+ { key: "homeTeam", label: "Home", maxWidth: 20 },
1998
+ { key: "awayTeam", label: "Away", maxWidth: 20 },
1999
+ { key: "homePoints", label: "H.Pts", maxWidth: 6 },
2000
+ { key: "awayPoints", label: "A.Pts", maxWidth: 6 },
2001
+ { key: "venue", label: "Venue", maxWidth: 24 }
2002
+ ];
2003
+ matchesCommand = defineCommand({
2004
+ meta: {
2005
+ name: "matches",
2006
+ description: "Fetch match results for a season"
2007
+ },
2008
+ args: {
2009
+ season: { type: "string", description: "Season year (e.g. 2025)", required: true },
2010
+ round: { type: "string", description: "Round number" },
2011
+ source: { type: "string", description: "Data source", default: "afl-api" },
2012
+ competition: {
2013
+ type: "string",
2014
+ description: "Competition code (AFLM or AFLW)",
2015
+ default: "AFLM"
2016
+ },
2017
+ json: { type: "boolean", description: "Output as JSON" },
2018
+ csv: { type: "boolean", description: "Output as CSV" },
2019
+ format: { type: "string", description: "Output format: table, json, csv" },
2020
+ full: { type: "boolean", description: "Show all columns in table output" }
2021
+ },
2022
+ async run({ args }) {
2023
+ const season = Number(args.season);
2024
+ const round = args.round ? Number(args.round) : void 0;
2025
+ const result = await withSpinner(
2026
+ "Fetching match results\u2026",
2027
+ () => fetchMatchResults({
2028
+ source: args.source,
2029
+ season,
2030
+ round,
2031
+ competition: args.competition
2032
+ })
2033
+ );
2034
+ if (!result.success) {
2035
+ throw result.error;
2036
+ }
2037
+ const data = result.data;
2038
+ showSummary(`Loaded ${data.length} matches for ${season}${round ? ` round ${round}` : ""}`);
2039
+ const formatOptions = {
2040
+ json: args.json,
2041
+ csv: args.csv,
2042
+ format: args.format,
2043
+ full: args.full,
2044
+ columns: DEFAULT_COLUMNS
2045
+ };
2046
+ console.log(formatOutput(data, formatOptions));
2047
+ }
2048
+ });
2049
+ }
2050
+ });
2051
+
2052
+ // src/cli/commands/stats.ts
2053
+ var stats_exports = {};
2054
+ __export(stats_exports, {
2055
+ statsCommand: () => statsCommand
2056
+ });
2057
+ import { defineCommand as defineCommand2 } from "citty";
2058
+ var DEFAULT_COLUMNS2, statsCommand;
2059
+ var init_stats = __esm({
2060
+ "src/cli/commands/stats.ts"() {
2061
+ "use strict";
2062
+ init_index();
2063
+ init_formatters();
2064
+ init_ui();
2065
+ DEFAULT_COLUMNS2 = [
2066
+ { key: "displayName", label: "Player", maxWidth: 22 },
2067
+ { key: "team", label: "Team", maxWidth: 18 },
2068
+ { key: "disposals", label: "Disp", maxWidth: 6 },
2069
+ { key: "kicks", label: "Kicks", maxWidth: 6 },
2070
+ { key: "handballs", label: "HB", maxWidth: 6 },
2071
+ { key: "marks", label: "Marks", maxWidth: 6 },
2072
+ { key: "goals", label: "Goals", maxWidth: 6 }
2073
+ ];
2074
+ statsCommand = defineCommand2({
2075
+ meta: {
2076
+ name: "stats",
2077
+ description: "Fetch player statistics for a season"
2078
+ },
2079
+ args: {
2080
+ season: { type: "string", description: "Season year (e.g. 2025)", required: true },
2081
+ round: { type: "string", description: "Round number" },
2082
+ "match-id": { type: "string", description: "Specific match ID" },
2083
+ source: { type: "string", description: "Data source", default: "afl-api" },
2084
+ competition: {
2085
+ type: "string",
2086
+ description: "Competition code (AFLM or AFLW)",
2087
+ default: "AFLM"
2088
+ },
2089
+ json: { type: "boolean", description: "Output as JSON" },
2090
+ csv: { type: "boolean", description: "Output as CSV" },
2091
+ format: { type: "string", description: "Output format: table, json, csv" },
2092
+ full: { type: "boolean", description: "Show all columns in table output" }
2093
+ },
2094
+ async run({ args }) {
2095
+ const season = Number(args.season);
2096
+ const round = args.round ? Number(args.round) : void 0;
2097
+ const matchId = args["match-id"];
2098
+ const result = await withSpinner(
2099
+ "Fetching player stats\u2026",
2100
+ () => fetchPlayerStats({
2101
+ source: args.source,
2102
+ season,
2103
+ round,
2104
+ matchId,
2105
+ competition: args.competition
2106
+ })
2107
+ );
2108
+ if (!result.success) {
2109
+ throw result.error;
2110
+ }
2111
+ const data = result.data;
2112
+ showSummary(
2113
+ `Loaded ${data.length} player stat lines for ${season}${round ? ` round ${round}` : ""}`
2114
+ );
2115
+ const formatOptions = {
2116
+ json: args.json,
2117
+ csv: args.csv,
2118
+ format: args.format,
2119
+ full: args.full,
2120
+ columns: DEFAULT_COLUMNS2
2121
+ };
2122
+ console.log(formatOutput(data, formatOptions));
2123
+ }
2124
+ });
2125
+ }
2126
+ });
2127
+
2128
+ // src/cli/commands/fixture.ts
2129
+ var fixture_exports = {};
2130
+ __export(fixture_exports, {
2131
+ fixtureCommand: () => fixtureCommand
2132
+ });
2133
+ import { defineCommand as defineCommand3 } from "citty";
2134
+ var DEFAULT_COLUMNS3, fixtureCommand;
2135
+ var init_fixture2 = __esm({
2136
+ "src/cli/commands/fixture.ts"() {
2137
+ "use strict";
2138
+ init_index();
2139
+ init_formatters();
2140
+ init_ui();
2141
+ DEFAULT_COLUMNS3 = [
2142
+ { key: "roundNumber", label: "Round", maxWidth: 6 },
2143
+ { key: "date", label: "Date", maxWidth: 16 },
2144
+ { key: "homeTeam", label: "Home", maxWidth: 20 },
2145
+ { key: "awayTeam", label: "Away", maxWidth: 20 },
2146
+ { key: "venue", label: "Venue", maxWidth: 24 }
2147
+ ];
2148
+ fixtureCommand = defineCommand3({
2149
+ meta: {
2150
+ name: "fixture",
2151
+ description: "Fetch fixture/schedule for a season"
2152
+ },
2153
+ args: {
2154
+ season: { type: "string", description: "Season year (e.g. 2025)", required: true },
2155
+ round: { type: "string", description: "Round number" },
2156
+ source: { type: "string", description: "Data source", default: "afl-api" },
2157
+ competition: {
2158
+ type: "string",
2159
+ description: "Competition code (AFLM or AFLW)",
2160
+ default: "AFLM"
2161
+ },
2162
+ json: { type: "boolean", description: "Output as JSON" },
2163
+ csv: { type: "boolean", description: "Output as CSV" },
2164
+ format: { type: "string", description: "Output format: table, json, csv" },
2165
+ full: { type: "boolean", description: "Show all columns in table output" }
2166
+ },
2167
+ async run({ args }) {
2168
+ const season = Number(args.season);
2169
+ const round = args.round ? Number(args.round) : void 0;
2170
+ const result = await withSpinner(
2171
+ "Fetching fixture\u2026",
2172
+ () => fetchFixture({
2173
+ source: args.source,
2174
+ season,
2175
+ round,
2176
+ competition: args.competition
2177
+ })
2178
+ );
2179
+ if (!result.success) {
2180
+ throw result.error;
2181
+ }
2182
+ const data = result.data;
2183
+ showSummary(`Loaded ${data.length} fixtures for ${season}${round ? ` round ${round}` : ""}`);
2184
+ const formatOptions = {
2185
+ json: args.json,
2186
+ csv: args.csv,
2187
+ format: args.format,
2188
+ full: args.full,
2189
+ columns: DEFAULT_COLUMNS3
2190
+ };
2191
+ console.log(formatOutput(data, formatOptions));
2192
+ }
2193
+ });
2194
+ }
2195
+ });
2196
+
2197
+ // src/cli/commands/ladder.ts
2198
+ var ladder_exports = {};
2199
+ __export(ladder_exports, {
2200
+ ladderCommand: () => ladderCommand
2201
+ });
2202
+ import { defineCommand as defineCommand4 } from "citty";
2203
+ var DEFAULT_COLUMNS4, ladderCommand;
2204
+ var init_ladder3 = __esm({
2205
+ "src/cli/commands/ladder.ts"() {
2206
+ "use strict";
2207
+ init_index();
2208
+ init_formatters();
2209
+ init_ui();
2210
+ DEFAULT_COLUMNS4 = [
2211
+ { key: "position", label: "Pos", maxWidth: 4 },
2212
+ { key: "team", label: "Team", maxWidth: 24 },
2213
+ { key: "wins", label: "W", maxWidth: 4 },
2214
+ { key: "losses", label: "L", maxWidth: 4 },
2215
+ { key: "draws", label: "D", maxWidth: 4 },
2216
+ { key: "percentage", label: "Pct", maxWidth: 8 },
2217
+ { key: "premiershipsPoints", label: "Pts", maxWidth: 5 }
2218
+ ];
2219
+ ladderCommand = defineCommand4({
2220
+ meta: {
2221
+ name: "ladder",
2222
+ description: "Fetch ladder standings for a season"
2223
+ },
2224
+ args: {
2225
+ season: { type: "string", description: "Season year (e.g. 2025)", required: true },
2226
+ round: { type: "string", description: "Round number" },
2227
+ source: { type: "string", description: "Data source", default: "afl-api" },
2228
+ competition: {
2229
+ type: "string",
2230
+ description: "Competition code (AFLM or AFLW)",
2231
+ default: "AFLM"
2232
+ },
2233
+ json: { type: "boolean", description: "Output as JSON" },
2234
+ csv: { type: "boolean", description: "Output as CSV" },
2235
+ format: { type: "string", description: "Output format: table, json, csv" },
2236
+ full: { type: "boolean", description: "Show all columns in table output" }
2237
+ },
2238
+ async run({ args }) {
2239
+ const season = Number(args.season);
2240
+ const round = args.round ? Number(args.round) : void 0;
2241
+ const result = await withSpinner(
2242
+ "Fetching ladder\u2026",
2243
+ () => fetchLadder({
2244
+ source: args.source,
2245
+ season,
2246
+ round,
2247
+ competition: args.competition
2248
+ })
2249
+ );
2250
+ if (!result.success) {
2251
+ throw result.error;
2252
+ }
2253
+ const data = result.data;
2254
+ showSummary(
2255
+ `Loaded ladder for ${season}${round ? ` round ${round}` : ""} (${data.entries.length} teams)`
2256
+ );
2257
+ const formatOptions = {
2258
+ json: args.json,
2259
+ csv: args.csv,
2260
+ format: args.format,
2261
+ full: args.full,
2262
+ columns: DEFAULT_COLUMNS4
2263
+ };
2264
+ console.log(formatOutput(data.entries, formatOptions));
2265
+ }
2266
+ });
2267
+ }
2268
+ });
2269
+
2270
+ // src/cli/commands/lineup.ts
2271
+ var lineup_exports = {};
2272
+ __export(lineup_exports, {
2273
+ lineupCommand: () => lineupCommand
2274
+ });
2275
+ import { defineCommand as defineCommand5 } from "citty";
2276
+ var DEFAULT_COLUMNS5, lineupCommand;
2277
+ var init_lineup3 = __esm({
2278
+ "src/cli/commands/lineup.ts"() {
2279
+ "use strict";
2280
+ init_index();
2281
+ init_formatters();
2282
+ init_ui();
2283
+ DEFAULT_COLUMNS5 = [
2284
+ { key: "matchId", label: "Match", maxWidth: 12 },
2285
+ { key: "homeTeam", label: "Home", maxWidth: 20 },
2286
+ { key: "awayTeam", label: "Away", maxWidth: 20 }
2287
+ ];
2288
+ lineupCommand = defineCommand5({
2289
+ meta: {
2290
+ name: "lineup",
2291
+ description: "Fetch match lineups for a round"
2292
+ },
2293
+ args: {
2294
+ season: { type: "string", description: "Season year (e.g. 2025)", required: true },
2295
+ round: { type: "string", description: "Round number", required: true },
2296
+ "match-id": { type: "string", description: "Specific match ID" },
2297
+ source: { type: "string", description: "Data source", default: "afl-api" },
2298
+ competition: {
2299
+ type: "string",
2300
+ description: "Competition code (AFLM or AFLW)",
2301
+ default: "AFLM"
2302
+ },
2303
+ json: { type: "boolean", description: "Output as JSON" },
2304
+ csv: { type: "boolean", description: "Output as CSV" },
2305
+ format: { type: "string", description: "Output format: table, json, csv" },
2306
+ full: { type: "boolean", description: "Show all columns in table output" }
2307
+ },
2308
+ async run({ args }) {
2309
+ const season = Number(args.season);
2310
+ const round = Number(args.round);
2311
+ const matchId = args["match-id"];
2312
+ const result = await withSpinner(
2313
+ "Fetching lineups\u2026",
2314
+ () => fetchLineup({
2315
+ source: args.source,
2316
+ season,
2317
+ round,
2318
+ matchId,
2319
+ competition: args.competition
2320
+ })
2321
+ );
2322
+ if (!result.success) {
2323
+ throw result.error;
2324
+ }
2325
+ const data = result.data;
2326
+ showSummary(`Loaded ${data.length} lineups for ${season} round ${round}`);
2327
+ const formatOptions = {
2328
+ json: args.json,
2329
+ csv: args.csv,
2330
+ format: args.format,
2331
+ full: args.full,
2332
+ columns: DEFAULT_COLUMNS5
2333
+ };
2334
+ console.log(formatOutput(data, formatOptions));
2335
+ }
2336
+ });
2337
+ }
2338
+ });
2339
+
2340
+ // src/cli/commands/squad.ts
2341
+ var squad_exports = {};
2342
+ __export(squad_exports, {
2343
+ squadCommand: () => squadCommand
2344
+ });
2345
+ import { defineCommand as defineCommand6 } from "citty";
2346
+ var DEFAULT_COLUMNS6, squadCommand;
2347
+ var init_squad = __esm({
2348
+ "src/cli/commands/squad.ts"() {
2349
+ "use strict";
2350
+ init_index();
2351
+ init_formatters();
2352
+ init_ui();
2353
+ DEFAULT_COLUMNS6 = [
2354
+ { key: "displayName", label: "Player", maxWidth: 24 },
2355
+ { key: "jumperNumber", label: "#", maxWidth: 4 },
2356
+ { key: "position", label: "Pos", maxWidth: 12 },
2357
+ { key: "heightCm", label: "Ht", maxWidth: 5 },
2358
+ { key: "weightKg", label: "Wt", maxWidth: 5 }
2359
+ ];
2360
+ squadCommand = defineCommand6({
2361
+ meta: {
2362
+ name: "squad",
2363
+ description: "Fetch team squad for a season"
2364
+ },
2365
+ args: {
2366
+ "team-id": { type: "string", description: "Team ID", required: true },
2367
+ season: { type: "string", description: "Season year (e.g. 2025)", required: true },
2368
+ competition: {
2369
+ type: "string",
2370
+ description: "Competition code (AFLM or AFLW)",
2371
+ default: "AFLM"
2372
+ },
2373
+ json: { type: "boolean", description: "Output as JSON" },
2374
+ csv: { type: "boolean", description: "Output as CSV" },
2375
+ format: { type: "string", description: "Output format: table, json, csv" },
2376
+ full: { type: "boolean", description: "Show all columns in table output" }
2377
+ },
2378
+ async run({ args }) {
2379
+ const teamId = args["team-id"];
2380
+ const season = Number(args.season);
2381
+ const result = await withSpinner(
2382
+ "Fetching squad\u2026",
2383
+ () => fetchSquad({
2384
+ teamId,
2385
+ season,
2386
+ competition: args.competition
2387
+ })
2388
+ );
2389
+ if (!result.success) {
2390
+ throw result.error;
2391
+ }
2392
+ const data = result.data;
2393
+ showSummary(`Loaded ${data.players.length} players for ${data.teamName} ${season}`);
2394
+ const formatOptions = {
2395
+ json: args.json,
2396
+ csv: args.csv,
2397
+ format: args.format,
2398
+ full: args.full,
2399
+ columns: DEFAULT_COLUMNS6
2400
+ };
2401
+ console.log(formatOutput(data.players, formatOptions));
2402
+ }
2403
+ });
2404
+ }
2405
+ });
2406
+
2407
+ // src/cli/commands/teams.ts
2408
+ var teams_exports = {};
2409
+ __export(teams_exports, {
2410
+ teamsCommand: () => teamsCommand
2411
+ });
2412
+ import { defineCommand as defineCommand7 } from "citty";
2413
+ var DEFAULT_COLUMNS7, teamsCommand;
2414
+ var init_teams2 = __esm({
2415
+ "src/cli/commands/teams.ts"() {
2416
+ "use strict";
2417
+ init_index();
2418
+ init_formatters();
2419
+ init_ui();
2420
+ DEFAULT_COLUMNS7 = [
2421
+ { key: "teamId", label: "ID", maxWidth: 8 },
2422
+ { key: "name", label: "Team", maxWidth: 24 },
2423
+ { key: "abbreviation", label: "Abbr", maxWidth: 6 },
2424
+ { key: "competition", label: "Comp", maxWidth: 6 }
2425
+ ];
2426
+ teamsCommand = defineCommand7({
2427
+ meta: {
2428
+ name: "teams",
2429
+ description: "Fetch team list"
2430
+ },
2431
+ args: {
2432
+ competition: { type: "string", description: "Competition code (AFLM or AFLW)" },
2433
+ "team-type": { type: "string", description: "Team type filter" },
2434
+ json: { type: "boolean", description: "Output as JSON" },
2435
+ csv: { type: "boolean", description: "Output as CSV" },
2436
+ format: { type: "string", description: "Output format: table, json, csv" },
2437
+ full: { type: "boolean", description: "Show all columns in table output" }
2438
+ },
2439
+ async run({ args }) {
2440
+ const result = await withSpinner(
2441
+ "Fetching teams\u2026",
2442
+ () => fetchTeams({
2443
+ competition: args.competition,
2444
+ teamType: args["team-type"]
2445
+ })
2446
+ );
2447
+ if (!result.success) {
2448
+ throw result.error;
2449
+ }
2450
+ const data = result.data;
2451
+ showSummary(`Loaded ${data.length} teams`);
2452
+ const formatOptions = {
2453
+ json: args.json,
2454
+ csv: args.csv,
2455
+ format: args.format,
2456
+ full: args.full,
2457
+ columns: DEFAULT_COLUMNS7
2458
+ };
2459
+ console.log(formatOutput(data, formatOptions));
2460
+ }
2461
+ });
2462
+ }
2463
+ });
46
2464
 
47
2465
  // src/cli.ts
48
- var main = defineCommand({
2466
+ init_errors();
2467
+ import { defineCommand as defineCommand8, runMain } from "citty";
2468
+ import pc2 from "picocolors";
2469
+ var main = defineCommand8({
49
2470
  meta: {
50
2471
  name: "fitzroy",
51
- version: "1.0.0",
52
- description: "CLI for fetching AFL data match results, player stats, fixtures, ladders, and more"
2472
+ version: "1.0.2",
2473
+ description: "CLI for fetching AFL data \u2014 match results, player stats, fixtures, ladders, and more"
53
2474
  },
54
2475
  subCommands: {
55
- matches: () => import("./shared/chunk-kr78ch1j.js").then((m) => m.matchesCommand),
56
- stats: () => import("./shared/chunk-ngvkaczn.js").then((m) => m.statsCommand),
57
- fixture: () => import("./shared/chunk-99nkfy8s.js").then((m) => m.fixtureCommand),
58
- ladder: () => import("./shared/chunk-d6fkap72.js").then((m) => m.ladderCommand),
59
- lineup: () => import("./shared/chunk-9zcjfgwe.js").then((m) => m.lineupCommand),
60
- squad: () => import("./shared/chunk-c7vawngt.js").then((m) => m.squadCommand),
61
- teams: () => import("./shared/chunk-b380x0p6.js").then((m) => m.teamsCommand)
2476
+ matches: () => Promise.resolve().then(() => (init_matches(), matches_exports)).then((m) => m.matchesCommand),
2477
+ stats: () => Promise.resolve().then(() => (init_stats(), stats_exports)).then((m) => m.statsCommand),
2478
+ fixture: () => Promise.resolve().then(() => (init_fixture2(), fixture_exports)).then((m) => m.fixtureCommand),
2479
+ ladder: () => Promise.resolve().then(() => (init_ladder3(), ladder_exports)).then((m) => m.ladderCommand),
2480
+ lineup: () => Promise.resolve().then(() => (init_lineup3(), lineup_exports)).then((m) => m.lineupCommand),
2481
+ squad: () => Promise.resolve().then(() => (init_squad(), squad_exports)).then((m) => m.squadCommand),
2482
+ teams: () => Promise.resolve().then(() => (init_teams2(), teams_exports)).then((m) => m.teamsCommand)
62
2483
  }
63
2484
  });
64
2485
  function formatError(error) {
65
2486
  if (error instanceof ValidationError && error.issues) {
66
- const issueLines = error.issues.map((i) => ` ${pc.yellow(i.path)}: ${i.message}`);
67
- return `${pc.red("Validation error:")}
68
- ${issueLines.join(`
69
- `)}`;
2487
+ const issueLines = error.issues.map((i) => ` ${pc2.yellow(i.path)}: ${i.message}`);
2488
+ return `${pc2.red("Validation error:")}
2489
+ ${issueLines.join("\n")}`;
70
2490
  }
71
2491
  if (error instanceof AflApiError) {
72
2492
  const status = error.statusCode ? ` (HTTP ${error.statusCode})` : "";
73
- return `${pc.red("AFL API error:")} ${error.message}${status}`;
2493
+ return `${pc2.red("AFL API error:")} ${error.message}${status}`;
74
2494
  }
75
2495
  if (error instanceof ScrapeError) {
76
2496
  const source = error.source ? ` [${error.source}]` : "";
77
- return `${pc.red("Scrape error:")} ${error.message}${source}`;
2497
+ return `${pc2.red("Scrape error:")} ${error.message}${source}`;
78
2498
  }
79
2499
  if (error instanceof UnsupportedSourceError) {
80
- return `${pc.red("Unsupported source:")} ${error.message}`;
2500
+ return `${pc2.red("Unsupported source:")} ${error.message}`;
81
2501
  }
82
2502
  if (error instanceof Error) {
83
- return `${pc.red("Error:")} ${error.message}`;
2503
+ return `${pc2.red("Error:")} ${error.message}`;
84
2504
  }
85
- return `${pc.red("Error:")} ${String(error)}`;
2505
+ return `${pc2.red("Error:")} ${String(error)}`;
86
2506
  }
87
2507
  runMain(main).catch((error) => {
88
2508
  console.error(formatError(error));