fitzroy 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -59,35 +59,40 @@ var init_result = __esm({
59
59
  });
60
60
 
61
61
  // src/lib/date-utils.ts
62
- function parseFootyWireDate(dateStr) {
62
+ function parseFootyWireDate(dateStr, defaultYear) {
63
63
  const trimmed = dateStr.trim();
64
64
  if (trimmed === "") {
65
65
  return null;
66
66
  }
67
67
  const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
68
68
  const normalised = withoutDow.replace(/-/g, " ");
69
- const match = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
70
- if (!match) {
71
- return null;
72
- }
73
- const [, dayStr, monthStr, yearStr] = match;
74
- if (!dayStr || !monthStr || !yearStr) {
75
- return null;
76
- }
77
- const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
78
- if (monthIndex === void 0) {
79
- return null;
80
- }
81
- const year = Number.parseInt(yearStr, 10);
82
- const day = Number.parseInt(dayStr, 10);
83
- const date = new Date(Date.UTC(year, monthIndex, day));
84
- if (Number.isNaN(date.getTime())) {
85
- return null;
69
+ const fullMatch = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
70
+ if (fullMatch) {
71
+ const [, dayStr, monthStr, yearStr] = fullMatch;
72
+ if (dayStr && monthStr && yearStr) {
73
+ return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
74
+ }
86
75
  }
87
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
88
- return null;
76
+ const shortMatch = /^(\d{1,2})\s+([A-Za-z]+)(?:\s+(\d{1,2}):(\d{2})(am|pm))?$/i.exec(normalised);
77
+ if (shortMatch && defaultYear != null) {
78
+ const [, dayStr, monthStr, hourStr, minStr, ampm] = shortMatch;
79
+ if (!dayStr || !monthStr) return null;
80
+ const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
81
+ if (monthIndex === void 0) return null;
82
+ const day = Number.parseInt(dayStr, 10);
83
+ const hasTime = hourStr && minStr && ampm;
84
+ if (!hasTime) {
85
+ return buildUtcDate(defaultYear, monthStr, day);
86
+ }
87
+ let aestHours = Number.parseInt(hourStr, 10);
88
+ const minutes = Number.parseInt(minStr, 10);
89
+ if (ampm.toLowerCase() === "pm" && aestHours < 12) aestHours += 12;
90
+ if (ampm.toLowerCase() === "am" && aestHours === 12) aestHours = 0;
91
+ const date = new Date(Date.UTC(defaultYear, monthIndex, day, aestHours - 10, minutes));
92
+ if (Number.isNaN(date.getTime())) return null;
93
+ return date;
89
94
  }
90
- return date;
95
+ return null;
91
96
  }
92
97
  function parseAflTablesDate(dateStr) {
93
98
  const trimmed = dateStr.trim();
@@ -622,7 +627,7 @@ function parseMatchList(html, year) {
622
627
  const scoreLink = scoreCell.find("a").attr("href") ?? "";
623
628
  const midMatch = /mid=(\d+)/.exec(scoreLink);
624
629
  const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
625
- const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
630
+ const date = parseFootyWireDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
626
631
  const homeGoals = Math.floor(homePoints / 6);
627
632
  const homeBehinds = homePoints - homeGoals * 6;
628
633
  const awayGoals = Math.floor(awayPoints / 6);
@@ -692,7 +697,7 @@ function parseFixtureList(html, year) {
692
697
  if (teamLinks.length < 2) return;
693
698
  const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
694
699
  const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
695
- const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
700
+ const date = parseFootyWireDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
696
701
  gameNumber++;
697
702
  const scoreCell = cells.length >= 5 ? $(cells[4]) : null;
698
703
  const scoreText = scoreCell?.text().trim() ?? "";
@@ -780,6 +785,12 @@ function mergeTeamAndOppStats(teamStats, oppStats) {
780
785
  function teamNameToFootyWireSlug(teamName) {
781
786
  return FOOTYWIRE_SLUG_MAP.get(teamName);
782
787
  }
788
+ function normaliseDob(raw) {
789
+ if (!raw) return null;
790
+ const parsed = parseFootyWireDate(raw);
791
+ if (parsed) return parsed.toISOString().slice(0, 10);
792
+ return raw;
793
+ }
783
794
  function parseFootyWirePlayerList(html, teamName) {
784
795
  const $ = cheerio2.load(html);
785
796
  const players = [];
@@ -821,7 +832,7 @@ function parseFootyWirePlayerList(html, teamName) {
821
832
  team: teamName,
822
833
  jumperNumber,
823
834
  position: position || null,
824
- dateOfBirth: dobText || null,
835
+ dateOfBirth: normaliseDob(dobText),
825
836
  heightCm,
826
837
  weightKg: null,
827
838
  gamesPlayed,
@@ -1290,9 +1301,30 @@ var init_coaches_votes = __esm({
1290
1301
  }
1291
1302
  });
1292
1303
 
1304
+ // src/lib/concurrency.ts
1305
+ async function batchedMap(items, fn, options) {
1306
+ const batchSize = options?.batchSize ?? 5;
1307
+ const delayMs = options?.delayMs ?? 0;
1308
+ const results = [];
1309
+ for (let i = 0; i < items.length; i += batchSize) {
1310
+ const batch = items.slice(i, i + batchSize);
1311
+ const batchResults = await Promise.all(batch.map(fn));
1312
+ results.push(...batchResults);
1313
+ if (delayMs > 0 && i + batchSize < items.length) {
1314
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1315
+ }
1316
+ }
1317
+ return results;
1318
+ }
1319
+ var init_concurrency = __esm({
1320
+ "src/lib/concurrency.ts"() {
1321
+ "use strict";
1322
+ }
1323
+ });
1324
+
1293
1325
  // src/lib/validation.ts
1294
1326
  import { z } from "zod/v4";
1295
- 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;
1327
+ var AflApiTokenSchema, CompetitionSchema, CompetitionListSchema, CompseasonSchema, CompseasonListSchema, RoundSchema, RoundListSchema, ScoreSchema, PeriodScoreSchema, TeamScoreSchema, CfsMatchTeamSchema, CfsMatchSchema, CfsScoreSchema, CfsVenueSchema, MatchItemSchema, MatchItemListSchema, CfsPlayerInnerSchema, statNum, PlayerGameStatsSchema, PlayerStatsItemSchema, PlayerStatsListSchema, RosterPlayerSchema, TeamPlayersSchema, MatchRosterSchema, TeamItemSchema, TeamListSchema, SquadPlayerInnerSchema, SquadPlayerItemSchema, SquadSchema, SquadListSchema, WinLossRecordSchema, LadderEntryRawSchema, LadderResponseSchema;
1296
1328
  var init_validation = __esm({
1297
1329
  "src/lib/validation.ts"() {
1298
1330
  "use strict";
@@ -1399,71 +1431,72 @@ var init_validation = __esm({
1399
1431
  captain: z.boolean().optional(),
1400
1432
  playerJumperNumber: z.number().optional()
1401
1433
  }).passthrough();
1434
+ statNum = z.number().nullable().optional();
1402
1435
  PlayerGameStatsSchema = z.object({
1403
- goals: z.number().optional(),
1404
- behinds: z.number().optional(),
1405
- kicks: z.number().optional(),
1406
- handballs: z.number().optional(),
1407
- disposals: z.number().optional(),
1408
- marks: z.number().optional(),
1409
- bounces: z.number().optional(),
1410
- tackles: z.number().optional(),
1411
- contestedPossessions: z.number().optional(),
1412
- uncontestedPossessions: z.number().optional(),
1413
- totalPossessions: z.number().optional(),
1414
- inside50s: z.number().optional(),
1415
- marksInside50: z.number().optional(),
1416
- contestedMarks: z.number().optional(),
1417
- hitouts: z.number().optional(),
1418
- onePercenters: z.number().optional(),
1419
- disposalEfficiency: z.number().optional(),
1420
- clangers: z.number().optional(),
1421
- freesFor: z.number().optional(),
1422
- freesAgainst: z.number().optional(),
1423
- dreamTeamPoints: z.number().optional(),
1436
+ goals: statNum,
1437
+ behinds: statNum,
1438
+ kicks: statNum,
1439
+ handballs: statNum,
1440
+ disposals: statNum,
1441
+ marks: statNum,
1442
+ bounces: statNum,
1443
+ tackles: statNum,
1444
+ contestedPossessions: statNum,
1445
+ uncontestedPossessions: statNum,
1446
+ totalPossessions: statNum,
1447
+ inside50s: statNum,
1448
+ marksInside50: statNum,
1449
+ contestedMarks: statNum,
1450
+ hitouts: statNum,
1451
+ onePercenters: statNum,
1452
+ disposalEfficiency: statNum,
1453
+ clangers: statNum,
1454
+ freesFor: statNum,
1455
+ freesAgainst: statNum,
1456
+ dreamTeamPoints: statNum,
1424
1457
  clearances: z.object({
1425
- centreClearances: z.number().optional(),
1426
- stoppageClearances: z.number().optional(),
1427
- totalClearances: z.number().optional()
1428
- }).passthrough().optional(),
1429
- rebound50s: z.number().optional(),
1430
- goalAssists: z.number().optional(),
1431
- goalAccuracy: z.number().optional(),
1432
- turnovers: z.number().optional(),
1433
- intercepts: z.number().optional(),
1434
- tacklesInside50: z.number().optional(),
1435
- shotsAtGoal: z.number().optional(),
1436
- metresGained: z.number().optional(),
1437
- scoreInvolvements: z.number().optional(),
1438
- ratingPoints: z.number().optional(),
1458
+ centreClearances: statNum,
1459
+ stoppageClearances: statNum,
1460
+ totalClearances: statNum
1461
+ }).passthrough().nullable().optional(),
1462
+ rebound50s: statNum,
1463
+ goalAssists: statNum,
1464
+ goalAccuracy: statNum,
1465
+ turnovers: statNum,
1466
+ intercepts: statNum,
1467
+ tacklesInside50: statNum,
1468
+ shotsAtGoal: statNum,
1469
+ metresGained: statNum,
1470
+ scoreInvolvements: statNum,
1471
+ ratingPoints: statNum,
1439
1472
  extendedStats: z.object({
1440
- effectiveDisposals: z.number().optional(),
1441
- effectiveKicks: z.number().optional(),
1442
- kickEfficiency: z.number().optional(),
1443
- kickToHandballRatio: z.number().optional(),
1444
- pressureActs: z.number().optional(),
1445
- defHalfPressureActs: z.number().optional(),
1446
- spoils: z.number().optional(),
1447
- hitoutsToAdvantage: z.number().optional(),
1448
- hitoutWinPercentage: z.number().optional(),
1449
- hitoutToAdvantageRate: z.number().optional(),
1450
- groundBallGets: z.number().optional(),
1451
- f50GroundBallGets: z.number().optional(),
1452
- interceptMarks: z.number().optional(),
1453
- marksOnLead: z.number().optional(),
1454
- contestedPossessionRate: z.number().optional(),
1455
- contestOffOneOnOnes: z.number().optional(),
1456
- contestOffWins: z.number().optional(),
1457
- contestOffWinsPercentage: z.number().optional(),
1458
- contestDefOneOnOnes: z.number().optional(),
1459
- contestDefLosses: z.number().optional(),
1460
- contestDefLossPercentage: z.number().optional(),
1461
- centreBounceAttendances: z.number().optional(),
1462
- kickins: z.number().optional(),
1463
- kickinsPlayon: z.number().optional(),
1464
- ruckContests: z.number().optional(),
1465
- scoreLaunches: z.number().optional()
1466
- }).passthrough().optional()
1473
+ effectiveDisposals: statNum,
1474
+ effectiveKicks: statNum,
1475
+ kickEfficiency: statNum,
1476
+ kickToHandballRatio: statNum,
1477
+ pressureActs: statNum,
1478
+ defHalfPressureActs: statNum,
1479
+ spoils: statNum,
1480
+ hitoutsToAdvantage: statNum,
1481
+ hitoutWinPercentage: statNum,
1482
+ hitoutToAdvantageRate: statNum,
1483
+ groundBallGets: statNum,
1484
+ f50GroundBallGets: statNum,
1485
+ interceptMarks: statNum,
1486
+ marksOnLead: statNum,
1487
+ contestedPossessionRate: statNum,
1488
+ contestOffOneOnOnes: statNum,
1489
+ contestOffWins: statNum,
1490
+ contestOffWinsPercentage: statNum,
1491
+ contestDefOneOnOnes: statNum,
1492
+ contestDefLosses: statNum,
1493
+ contestDefLossPercentage: statNum,
1494
+ centreBounceAttendances: statNum,
1495
+ kickins: statNum,
1496
+ kickinsPlayon: statNum,
1497
+ ruckContests: statNum,
1498
+ scoreLaunches: statNum
1499
+ }).passthrough().nullable().optional()
1467
1500
  }).passthrough();
1468
1501
  PlayerStatsItemSchema = z.object({
1469
1502
  player: z.object({
@@ -1476,7 +1509,7 @@ var init_validation = __esm({
1476
1509
  teamId: z.string(),
1477
1510
  playerStats: z.object({
1478
1511
  stats: PlayerGameStatsSchema,
1479
- timeOnGroundPercentage: z.number().optional()
1512
+ timeOnGroundPercentage: z.number().nullable().optional()
1480
1513
  }).passthrough()
1481
1514
  }).passthrough();
1482
1515
  PlayerStatsListSchema = z.object({
@@ -1577,6 +1610,7 @@ var TOKEN_URL, API_BASE, CFS_BASE, AflApiClient;
1577
1610
  var init_afl_api = __esm({
1578
1611
  "src/sources/afl-api.ts"() {
1579
1612
  "use strict";
1613
+ init_concurrency();
1580
1614
  init_errors();
1581
1615
  init_result();
1582
1616
  init_validation();
@@ -1587,6 +1621,7 @@ var init_afl_api = __esm({
1587
1621
  fetchFn;
1588
1622
  tokenUrl;
1589
1623
  cachedToken = null;
1624
+ pendingAuth = null;
1590
1625
  constructor(options) {
1591
1626
  this.fetchFn = options?.fetchFn ?? globalThis.fetch;
1592
1627
  this.tokenUrl = options?.tokenUrl ?? TOKEN_URL;
@@ -1594,9 +1629,21 @@ var init_afl_api = __esm({
1594
1629
  /**
1595
1630
  * Authenticate with the WMCTok token endpoint and cache the token.
1596
1631
  *
1632
+ * Concurrent callers share the same in-flight request to avoid
1633
+ * redundant token fetches (thundering herd prevention).
1634
+ *
1597
1635
  * @returns The access token on success, or an error Result.
1598
1636
  */
1599
1637
  async authenticate() {
1638
+ if (this.pendingAuth) {
1639
+ return this.pendingAuth;
1640
+ }
1641
+ this.pendingAuth = this.doAuthenticate().finally(() => {
1642
+ this.pendingAuth = null;
1643
+ });
1644
+ return this.pendingAuth;
1645
+ }
1646
+ async doAuthenticate() {
1600
1647
  try {
1601
1648
  const response = await this.fetchFn(this.tokenUrl, {
1602
1649
  method: "POST",
@@ -1851,7 +1898,7 @@ var init_afl_api = __esm({
1851
1898
  return roundsResult;
1852
1899
  }
1853
1900
  const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
1854
- const results = await Promise.all(providerIds.map((id) => this.fetchRoundMatchItems(id)));
1901
+ const results = await batchedMap(providerIds, (id) => this.fetchRoundMatchItems(id));
1855
1902
  const allItems = [];
1856
1903
  for (const result of results) {
1857
1904
  if (!result.success) {
@@ -2209,8 +2256,9 @@ async function fetchFixture(query) {
2209
2256
  const roundProviderIds = roundsResult.data.flatMap(
2210
2257
  (r) => r.providerId ? [{ providerId: r.providerId, roundNumber: r.roundNumber }] : []
2211
2258
  );
2212
- const roundResults = await Promise.all(
2213
- roundProviderIds.map((r) => client.fetchRoundMatchItems(r.providerId))
2259
+ const roundResults = await batchedMap(
2260
+ roundProviderIds,
2261
+ (r) => client.fetchRoundMatchItems(r.providerId)
2214
2262
  );
2215
2263
  const fixtures = [];
2216
2264
  for (let i = 0; i < roundResults.length; i++) {
@@ -2226,6 +2274,7 @@ async function fetchFixture(query) {
2226
2274
  var init_fixture = __esm({
2227
2275
  "src/api/fixture.ts"() {
2228
2276
  "use strict";
2277
+ init_concurrency();
2229
2278
  init_errors();
2230
2279
  init_result();
2231
2280
  init_team_mapping();
@@ -2486,6 +2535,7 @@ function parseAflTablesTeamStats(html, year) {
2486
2535
  $(rows[0]).find("td, th").each((_ci, cell) => {
2487
2536
  headers.push($(cell).text().trim());
2488
2537
  });
2538
+ const gpColIdx = headers.findIndex((h, i) => i > 0 && GP_HEADERS.has(h.toLowerCase()));
2489
2539
  for (let ri = 1; ri < rows.length; ri++) {
2490
2540
  const cells = $(rows[ri]).find("td");
2491
2541
  if (cells.length < 3) continue;
@@ -2498,7 +2548,12 @@ function parseAflTablesTeamStats(html, year) {
2498
2548
  }
2499
2549
  const entry = teamMap.get(teamName);
2500
2550
  if (!entry) continue;
2551
+ if (gpColIdx >= 0 && suffix === "_for") {
2552
+ const gpVal = Number.parseFloat($(cells[gpColIdx]).text().trim().replace(/,/g, "")) || 0;
2553
+ entry.gamesPlayed = gpVal;
2554
+ }
2501
2555
  for (let ci = 1; ci < cells.length; ci++) {
2556
+ if (ci === gpColIdx) continue;
2502
2557
  const header = headers[ci];
2503
2558
  if (!header) continue;
2504
2559
  const value = Number.parseFloat($(cells[ci]).text().trim().replace(/,/g, "")) || 0;
@@ -2574,7 +2629,7 @@ function parseAflTablesPlayerList(html, teamName) {
2574
2629
  });
2575
2630
  return players;
2576
2631
  }
2577
- var AFL_TABLES_BASE, AflTablesClient, AFL_TABLES_SLUG_MAP;
2632
+ var AFL_TABLES_BASE, AflTablesClient, GP_HEADERS, AFL_TABLES_SLUG_MAP;
2578
2633
  var init_afl_tables = __esm({
2579
2634
  "src/sources/afl-tables.ts"() {
2580
2635
  "use strict";
@@ -2756,6 +2811,7 @@ var init_afl_tables = __esm({
2756
2811
  }
2757
2812
  }
2758
2813
  };
2814
+ GP_HEADERS = /* @__PURE__ */ new Set(["gm", "gp", "p", "mp", "games"]);
2759
2815
  AFL_TABLES_SLUG_MAP = /* @__PURE__ */ new Map([
2760
2816
  ["Adelaide Crows", "adelaide"],
2761
2817
  ["Brisbane Lions", "brisbane"],
@@ -3015,8 +3071,9 @@ async function fetchLineup(query) {
3015
3071
  if (matchItems.data.length === 0) {
3016
3072
  return err(new AflApiError(`No matches found for round ${query.round}`));
3017
3073
  }
3018
- const rosterResults = await Promise.all(
3019
- matchItems.data.map((item) => client.fetchMatchRoster(item.match.matchId))
3074
+ const rosterResults = await batchedMap(
3075
+ matchItems.data,
3076
+ (item) => client.fetchMatchRoster(item.match.matchId)
3020
3077
  );
3021
3078
  const lineups = [];
3022
3079
  for (const rosterResult of rosterResults) {
@@ -3028,6 +3085,7 @@ async function fetchLineup(query) {
3028
3085
  var init_lineup2 = __esm({
3029
3086
  "src/api/lineup.ts"() {
3030
3087
  "use strict";
3088
+ init_concurrency();
3031
3089
  init_errors();
3032
3090
  init_result();
3033
3091
  init_afl_api();
@@ -3310,15 +3368,26 @@ async function fetchPlayerStats(query) {
3310
3368
  case "afl-api": {
3311
3369
  const client = new AflApiClient();
3312
3370
  if (query.matchId) {
3313
- const result = await client.fetchPlayerStats(query.matchId);
3314
- if (!result.success) return result;
3371
+ const [rosterResult, statsResult] = await Promise.all([
3372
+ client.fetchMatchRoster(query.matchId),
3373
+ client.fetchPlayerStats(query.matchId)
3374
+ ]);
3375
+ if (!statsResult.success) return statsResult;
3376
+ const teamIdMap2 = /* @__PURE__ */ new Map();
3377
+ if (rosterResult.success) {
3378
+ const match = rosterResult.data.match;
3379
+ teamIdMap2.set(match.homeTeamId, match.homeTeam.name);
3380
+ teamIdMap2.set(match.awayTeamId, match.awayTeam.name);
3381
+ }
3315
3382
  return ok(
3316
3383
  transformPlayerStats(
3317
- result.data,
3384
+ statsResult.data,
3318
3385
  query.matchId,
3319
3386
  query.season,
3320
3387
  query.round ?? 0,
3321
- competition
3388
+ competition,
3389
+ "afl-api",
3390
+ teamIdMap2.size > 0 ? teamIdMap2 : void 0
3322
3391
  )
3323
3392
  );
3324
3393
  }
@@ -3335,8 +3404,9 @@ async function fetchPlayerStats(query) {
3335
3404
  teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
3336
3405
  teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
3337
3406
  }
3338
- const statsResults = await Promise.all(
3339
- matchItemsResult.data.map((item) => client.fetchPlayerStats(item.match.matchId))
3407
+ const statsResults = await batchedMap(
3408
+ matchItemsResult.data,
3409
+ (item) => client.fetchPlayerStats(item.match.matchId)
3340
3410
  );
3341
3411
  const allStats = [];
3342
3412
  for (let i = 0; i < statsResults.length; i++) {
@@ -3404,6 +3474,7 @@ async function fetchPlayerStats(query) {
3404
3474
  var init_player_stats2 = __esm({
3405
3475
  "src/api/player-stats.ts"() {
3406
3476
  "use strict";
3477
+ init_concurrency();
3407
3478
  init_errors();
3408
3479
  init_result();
3409
3480
  init_afl_api();
@@ -3526,6 +3597,94 @@ var init_index = __esm({
3526
3597
  }
3527
3598
  });
3528
3599
 
3600
+ // src/cli/flags.ts
3601
+ var SEASON_FLAG, OPTIONAL_SEASON_FLAG, ROUND_FLAG, REQUIRED_ROUND_FLAG, SOURCE_FLAG, COMPETITION_FLAG, OPTIONAL_COMPETITION_FLAG, OUTPUT_FLAGS, REQUIRED_TEAM_FLAG, TEAM_FLAG, PLAYER_FLAG;
3602
+ var init_flags = __esm({
3603
+ "src/cli/flags.ts"() {
3604
+ "use strict";
3605
+ SEASON_FLAG = {
3606
+ season: {
3607
+ type: "string",
3608
+ description: "Season year (e.g. 2025)",
3609
+ required: true,
3610
+ alias: "s"
3611
+ }
3612
+ };
3613
+ OPTIONAL_SEASON_FLAG = {
3614
+ season: {
3615
+ type: "string",
3616
+ description: "Season year (e.g. 2025)",
3617
+ alias: "s"
3618
+ }
3619
+ };
3620
+ ROUND_FLAG = {
3621
+ round: {
3622
+ type: "string",
3623
+ description: "Round number",
3624
+ alias: "r"
3625
+ }
3626
+ };
3627
+ REQUIRED_ROUND_FLAG = {
3628
+ round: {
3629
+ type: "string",
3630
+ description: "Round number",
3631
+ required: true,
3632
+ alias: "r"
3633
+ }
3634
+ };
3635
+ SOURCE_FLAG = {
3636
+ source: {
3637
+ type: "string",
3638
+ description: "Data source",
3639
+ default: "afl-api"
3640
+ }
3641
+ };
3642
+ COMPETITION_FLAG = {
3643
+ competition: {
3644
+ type: "string",
3645
+ description: "Competition code (AFLM or AFLW)",
3646
+ default: "AFLM",
3647
+ alias: "c"
3648
+ }
3649
+ };
3650
+ OPTIONAL_COMPETITION_FLAG = {
3651
+ competition: {
3652
+ type: "string",
3653
+ description: "Competition code (AFLM or AFLW)",
3654
+ alias: "c"
3655
+ }
3656
+ };
3657
+ OUTPUT_FLAGS = {
3658
+ json: { type: "boolean", description: "Output as JSON", alias: "j" },
3659
+ csv: { type: "boolean", description: "Output as CSV" },
3660
+ format: { type: "string", description: "Output format: table, json, csv" },
3661
+ full: { type: "boolean", description: "Show all columns in table output" }
3662
+ };
3663
+ REQUIRED_TEAM_FLAG = {
3664
+ team: {
3665
+ type: "string",
3666
+ description: "Team name, abbreviation, or ID (e.g. Carlton, CARL, 5)",
3667
+ required: true,
3668
+ alias: "t"
3669
+ }
3670
+ };
3671
+ TEAM_FLAG = {
3672
+ team: {
3673
+ type: "string",
3674
+ description: "Filter by team name",
3675
+ alias: "t"
3676
+ }
3677
+ };
3678
+ PLAYER_FLAG = {
3679
+ player: {
3680
+ type: "string",
3681
+ description: "Filter by player name",
3682
+ alias: "p"
3683
+ }
3684
+ };
3685
+ }
3686
+ });
3687
+
3529
3688
  // src/cli/formatters/csv.ts
3530
3689
  function escapeField(value) {
3531
3690
  if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
@@ -3570,7 +3729,7 @@ var init_json = __esm({
3570
3729
  // src/cli/formatters/table.ts
3571
3730
  function toDisplayValue(value) {
3572
3731
  if (value === null || value === void 0) return "-";
3573
- if (value instanceof Date) return value.toISOString().slice(0, 16).replace("T", " ");
3732
+ if (value instanceof Date) return AEST_COMPACT_FORMATTER.format(value);
3574
3733
  if (typeof value === "object") return JSON.stringify(value);
3575
3734
  return String(value);
3576
3735
  }
@@ -3644,9 +3803,18 @@ function formatTable(data, options = {}) {
3644
3803
  });
3645
3804
  return [header, separator, ...rows].join("\n");
3646
3805
  }
3806
+ var AEST_COMPACT_FORMATTER;
3647
3807
  var init_table = __esm({
3648
3808
  "src/cli/formatters/table.ts"() {
3649
3809
  "use strict";
3810
+ AEST_COMPACT_FORMATTER = new Intl.DateTimeFormat("en-AU", {
3811
+ timeZone: "Australia/Melbourne",
3812
+ day: "numeric",
3813
+ month: "short",
3814
+ hour: "numeric",
3815
+ minute: "2-digit",
3816
+ hour12: true
3817
+ });
3650
3818
  }
3651
3819
  });
3652
3820
 
@@ -3686,7 +3854,7 @@ var init_formatters = __esm({
3686
3854
 
3687
3855
  // src/cli/ui.ts
3688
3856
  import { spinner } from "@clack/prompts";
3689
- import pc from "picocolors";
3857
+ import pc2 from "picocolors";
3690
3858
  async function withSpinner(message, fn) {
3691
3859
  if (!isTTY) {
3692
3860
  return fn();
@@ -3704,7 +3872,7 @@ async function withSpinner(message, fn) {
3704
3872
  }
3705
3873
  function showSummary(message) {
3706
3874
  if (!isTTY) return;
3707
- console.error(pc.dim(message));
3875
+ console.error(pc2.dim(message));
3708
3876
  }
3709
3877
  var isTTY;
3710
3878
  var init_ui = __esm({
@@ -3714,6 +3882,109 @@ var init_ui = __esm({
3714
3882
  }
3715
3883
  });
3716
3884
 
3885
+ // src/cli/validation.ts
3886
+ function validateSeason(raw) {
3887
+ const season = Number(raw);
3888
+ if (Number.isNaN(season) || !Number.isInteger(season)) {
3889
+ throw new Error(`Invalid season: "${raw}" \u2014 season must be a number (e.g. 2025)`);
3890
+ }
3891
+ if (season < 1897 || season > 2100) {
3892
+ throw new Error(`Invalid season: ${season} \u2014 must be between 1897 and 2100`);
3893
+ }
3894
+ return season;
3895
+ }
3896
+ function validateOptionalSeason(raw) {
3897
+ if (raw != null) return validateSeason(raw);
3898
+ return void 0;
3899
+ }
3900
+ function resolveDefaultSeason(competition = "AFLM") {
3901
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
3902
+ return competition === "AFLW" ? year - 1 : year;
3903
+ }
3904
+ function validateRound(raw) {
3905
+ const round = Number(raw);
3906
+ if (Number.isNaN(round) || !Number.isInteger(round) || round < 0) {
3907
+ throw new Error(`Invalid round: "${raw}" \u2014 round must be a non-negative integer`);
3908
+ }
3909
+ return round;
3910
+ }
3911
+ function validateFormat(raw) {
3912
+ if (raw == null) return void 0;
3913
+ const lower = raw.toLowerCase();
3914
+ if (VALID_FORMATS.includes(lower)) {
3915
+ return lower;
3916
+ }
3917
+ throw new Error(`Invalid format: "${raw}" \u2014 valid formats are: ${VALID_FORMATS.join(", ")}`);
3918
+ }
3919
+ function validateCompetition(raw) {
3920
+ const upper = raw.toUpperCase();
3921
+ if (upper === "AFLM" || upper === "AFLW") {
3922
+ return upper;
3923
+ }
3924
+ throw new Error(
3925
+ `Invalid competition: "${raw}" \u2014 valid values are: ${VALID_COMPETITIONS.join(", ")}`
3926
+ );
3927
+ }
3928
+ function validateOptionalCompetition(raw) {
3929
+ if (raw == null) return void 0;
3930
+ return validateCompetition(raw);
3931
+ }
3932
+ function validateSource(raw) {
3933
+ if (VALID_SOURCES.includes(raw)) {
3934
+ return raw;
3935
+ }
3936
+ throw new Error(`Invalid source: "${raw}" \u2014 valid sources are: ${VALID_SOURCES.join(", ")}`);
3937
+ }
3938
+ function resolveTeamIdentifier(raw, teams) {
3939
+ const trimmed = raw.trim();
3940
+ if (/^\d+$/.test(trimmed)) {
3941
+ return trimmed;
3942
+ }
3943
+ const canonical = normaliseTeamName(trimmed);
3944
+ const byCanonical = teams.find((t) => t.name === canonical);
3945
+ if (byCanonical) return byCanonical.teamId;
3946
+ const lower = trimmed.toLowerCase();
3947
+ const byName = teams.find((t) => t.name.toLowerCase() === lower);
3948
+ if (byName) return byName.teamId;
3949
+ const byAbbrev = teams.find((t) => t.abbreviation.toLowerCase() === lower);
3950
+ if (byAbbrev) return byAbbrev.teamId;
3951
+ const validNames = teams.map((t) => `${t.name} (${t.abbreviation})`).join(", ");
3952
+ throw new Error(`Unknown team: "${raw}" \u2014 valid teams are: ${validNames}`);
3953
+ }
3954
+ function resolveMatchByTeam(teamSearch, matchItems) {
3955
+ const normalised = normaliseTeamName(teamSearch);
3956
+ const lower = teamSearch.toLowerCase();
3957
+ const matches = matchItems.filter((item) => {
3958
+ const home = item.match.homeTeam.name;
3959
+ const away = item.match.awayTeam.name;
3960
+ return normaliseTeamName(home) === normalised || normaliseTeamName(away) === normalised || home.toLowerCase().includes(lower) || away.toLowerCase().includes(lower);
3961
+ });
3962
+ const singleMatch = matches[0];
3963
+ if (matches.length === 1 && singleMatch) {
3964
+ return singleMatch.match.matchId;
3965
+ }
3966
+ if (matches.length === 0) {
3967
+ const available = matchItems.map((item) => `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`).join(", ");
3968
+ throw new Error(
3969
+ `No match found for "${teamSearch}" in this round. Available matches: ${available}`
3970
+ );
3971
+ }
3972
+ const ambiguous = matches.map((item) => `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`).join(", ");
3973
+ throw new Error(
3974
+ `Multiple matches found for "${teamSearch}": ${ambiguous}. Please be more specific.`
3975
+ );
3976
+ }
3977
+ var VALID_SOURCES, VALID_COMPETITIONS, VALID_FORMATS;
3978
+ var init_validation2 = __esm({
3979
+ "src/cli/validation.ts"() {
3980
+ "use strict";
3981
+ init_team_mapping();
3982
+ VALID_SOURCES = ["afl-api", "footywire", "afl-tables", "squiggle"];
3983
+ VALID_COMPETITIONS = ["AFLM", "AFLW"];
3984
+ VALID_FORMATS = ["table", "json", "csv"];
3985
+ }
3986
+ });
3987
+
3717
3988
  // src/cli/commands/matches.ts
3718
3989
  var matches_exports = {};
3719
3990
  __export(matches_exports, {
@@ -3725,8 +3996,10 @@ var init_matches = __esm({
3725
3996
  "src/cli/commands/matches.ts"() {
3726
3997
  "use strict";
3727
3998
  init_index();
3999
+ init_flags();
3728
4000
  init_formatters();
3729
4001
  init_ui();
4002
+ init_validation2();
3730
4003
  DEFAULT_COLUMNS = [
3731
4004
  { key: "date", label: "Date", maxWidth: 16 },
3732
4005
  { key: "roundNumber", label: "Round", maxWidth: 6 },
@@ -3742,30 +4015,21 @@ var init_matches = __esm({
3742
4015
  description: "Fetch match results for a season"
3743
4016
  },
3744
4017
  args: {
3745
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
3746
- round: { type: "string", description: "Round number" },
3747
- source: { type: "string", description: "Data source", default: "afl-api" },
3748
- competition: {
3749
- type: "string",
3750
- description: "Competition code (AFLM or AFLW)",
3751
- default: "AFLM"
3752
- },
3753
- json: { type: "boolean", description: "Output as JSON" },
3754
- csv: { type: "boolean", description: "Output as CSV" },
3755
- format: { type: "string", description: "Output format: table, json, csv" },
3756
- full: { type: "boolean", description: "Show all columns in table output" }
4018
+ ...SEASON_FLAG,
4019
+ ...ROUND_FLAG,
4020
+ ...SOURCE_FLAG,
4021
+ ...COMPETITION_FLAG,
4022
+ ...OUTPUT_FLAGS
3757
4023
  },
3758
4024
  async run({ args }) {
3759
- const season = Number(args.season);
3760
- const round = args.round ? Number(args.round) : void 0;
4025
+ const season = validateSeason(args.season);
4026
+ const round = args.round ? validateRound(args.round) : void 0;
4027
+ const source = validateSource(args.source);
4028
+ const competition = validateCompetition(args.competition);
4029
+ const format = validateFormat(args.format);
3761
4030
  const result = await withSpinner(
3762
4031
  "Fetching match results\u2026",
3763
- () => fetchMatchResults({
3764
- source: args.source,
3765
- season,
3766
- round,
3767
- competition: args.competition
3768
- })
4032
+ () => fetchMatchResults({ source, season, round, competition })
3769
4033
  );
3770
4034
  if (!result.success) {
3771
4035
  throw result.error;
@@ -3775,7 +4039,7 @@ var init_matches = __esm({
3775
4039
  const formatOptions = {
3776
4040
  json: args.json,
3777
4041
  csv: args.csv,
3778
- format: args.format,
4042
+ format,
3779
4043
  full: args.full,
3780
4044
  columns: DEFAULT_COLUMNS
3781
4045
  };
@@ -3785,6 +4049,203 @@ var init_matches = __esm({
3785
4049
  }
3786
4050
  });
3787
4051
 
4052
+ // src/lib/fuzzy.ts
4053
+ function levenshteinDistance(a, b) {
4054
+ const la = a.length;
4055
+ const lb = b.length;
4056
+ if (la === 0) return lb;
4057
+ if (lb === 0) return la;
4058
+ if (la < lb) return levenshteinDistance(b, a);
4059
+ const row = Array.from({ length: lb + 1 }, (_, i) => i);
4060
+ for (let i = 1; i <= la; i++) {
4061
+ let prev = i;
4062
+ for (let j = 1; j <= lb; j++) {
4063
+ const current = row[j - 1];
4064
+ const rowJ = row[j];
4065
+ if (current === void 0 || rowJ === void 0) continue;
4066
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4067
+ const val = Math.min(rowJ + 1, prev + 1, current + cost);
4068
+ row[j - 1] = prev;
4069
+ prev = val;
4070
+ }
4071
+ row[lb] = prev;
4072
+ }
4073
+ return row[lb] ?? 0;
4074
+ }
4075
+ function fuzzySearch(query, candidates, keySelector, options) {
4076
+ const maxResults = options?.maxResults ?? 10;
4077
+ const threshold = options?.threshold ?? 0.4;
4078
+ const lowerQuery = query.toLowerCase();
4079
+ const results = [];
4080
+ for (const item of candidates) {
4081
+ const key = keySelector(item).toLowerCase();
4082
+ if (key === lowerQuery) {
4083
+ results.push({ item, score: 0 });
4084
+ continue;
4085
+ }
4086
+ if (key.startsWith(lowerQuery)) {
4087
+ results.push({ item, score: 0.1 });
4088
+ continue;
4089
+ }
4090
+ if (key.includes(lowerQuery)) {
4091
+ results.push({ item, score: 0.3 });
4092
+ continue;
4093
+ }
4094
+ const maxLen = Math.max(lowerQuery.length, key.length);
4095
+ if (maxLen === 0) continue;
4096
+ const distance = levenshteinDistance(lowerQuery, key);
4097
+ const normalised = distance / maxLen;
4098
+ if (normalised <= threshold) {
4099
+ const score = 0.4 + normalised / threshold * 0.6;
4100
+ results.push({ item, score });
4101
+ }
4102
+ }
4103
+ results.sort((a, b) => {
4104
+ if (a.score !== b.score) return a.score - b.score;
4105
+ return keySelector(a.item).localeCompare(keySelector(b.item));
4106
+ });
4107
+ return results.slice(0, maxResults);
4108
+ }
4109
+ var init_fuzzy = __esm({
4110
+ "src/lib/fuzzy.ts"() {
4111
+ "use strict";
4112
+ }
4113
+ });
4114
+
4115
+ // src/cli/resolvers.ts
4116
+ import { isCancel, select } from "@clack/prompts";
4117
+ async function resolveTeamOrPrompt(query, teams) {
4118
+ try {
4119
+ return resolveTeamIdentifier(query, teams);
4120
+ } catch {
4121
+ }
4122
+ const matches = fuzzySearch(query.trim(), teams, (t) => t.name, {
4123
+ maxResults: 5,
4124
+ threshold: 0.4
4125
+ });
4126
+ const abbrevMatches = fuzzySearch(query.trim(), teams, (t) => t.abbreviation, {
4127
+ maxResults: 5,
4128
+ threshold: 0.4
4129
+ });
4130
+ const seen = new Set(matches.map((m) => m.item.teamId));
4131
+ for (const m of abbrevMatches) {
4132
+ if (!seen.has(m.item.teamId)) {
4133
+ matches.push(m);
4134
+ seen.add(m.item.teamId);
4135
+ }
4136
+ }
4137
+ matches.sort((a, b) => a.score - b.score);
4138
+ return disambiguate(
4139
+ query.trim(),
4140
+ matches.map((m) => ({ value: m.item.teamId, label: m.item.name, score: m.score })),
4141
+ teams.map((t) => `${t.name} (${t.abbreviation})`),
4142
+ "team"
4143
+ );
4144
+ }
4145
+ async function resolveTeamNameOrPrompt(query, teamNames) {
4146
+ const trimmed = query.trim();
4147
+ const canonical = normaliseTeamName(trimmed);
4148
+ const candidates = teamNames ?? [...AFL_SENIOR_TEAMS];
4149
+ if (candidates.includes(canonical)) {
4150
+ return canonical;
4151
+ }
4152
+ const items = candidates.map((name) => ({ name }));
4153
+ const matches = fuzzySearch(trimmed, items, (t) => t.name, {
4154
+ maxResults: 5,
4155
+ threshold: 0.4
4156
+ });
4157
+ return disambiguate(
4158
+ trimmed,
4159
+ matches.map((m) => ({ value: m.item.name, label: m.item.name, score: m.score })),
4160
+ candidates,
4161
+ "team"
4162
+ );
4163
+ }
4164
+ async function resolveMatchOrPrompt(query, matchItems) {
4165
+ try {
4166
+ return resolveMatchByTeam(query, matchItems);
4167
+ } catch {
4168
+ }
4169
+ const labelledItems = matchItems.map((item) => ({
4170
+ item,
4171
+ label: `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`
4172
+ }));
4173
+ const matches = fuzzySearch(query, labelledItems, (l) => l.label, {
4174
+ maxResults: 5,
4175
+ threshold: 0.5
4176
+ });
4177
+ const homeMatches = fuzzySearch(query, matchItems, (i) => i.match.homeTeam.name, {
4178
+ maxResults: 5,
4179
+ threshold: 0.4
4180
+ });
4181
+ const awayMatches = fuzzySearch(query, matchItems, (i) => i.match.awayTeam.name, {
4182
+ maxResults: 5,
4183
+ threshold: 0.4
4184
+ });
4185
+ const seen = new Set(matches.map((m) => m.item.item.match.matchId));
4186
+ for (const m of homeMatches) {
4187
+ if (!seen.has(m.item.match.matchId)) {
4188
+ const label = `${m.item.match.homeTeam.name} vs ${m.item.match.awayTeam.name}`;
4189
+ matches.push({ item: { item: m.item, label }, score: m.score });
4190
+ seen.add(m.item.match.matchId);
4191
+ }
4192
+ }
4193
+ for (const m of awayMatches) {
4194
+ if (!seen.has(m.item.match.matchId)) {
4195
+ const label = `${m.item.match.homeTeam.name} vs ${m.item.match.awayTeam.name}`;
4196
+ matches.push({ item: { item: m.item, label }, score: m.score });
4197
+ seen.add(m.item.match.matchId);
4198
+ }
4199
+ }
4200
+ matches.sort((a, b) => a.score - b.score);
4201
+ const available = matchItems.map(
4202
+ (item) => `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`
4203
+ );
4204
+ return disambiguate(
4205
+ query,
4206
+ matches.map((m) => ({
4207
+ value: m.item.item.match.matchId,
4208
+ label: m.item.label,
4209
+ score: m.score
4210
+ })),
4211
+ available,
4212
+ "match"
4213
+ );
4214
+ }
4215
+ async function disambiguate(query, options, allLabels, entityName) {
4216
+ const best = options[0];
4217
+ if (!best) {
4218
+ throw new Error(
4219
+ `No ${entityName} found for "${query}". Valid options: ${allLabels.join(", ")}`
4220
+ );
4221
+ }
4222
+ if (best.score < 0.2 || options.length === 1) {
4223
+ return best.value;
4224
+ }
4225
+ if (isTTY2) {
4226
+ const choice = await select({
4227
+ message: `Multiple ${entityName}s matched "${query}". Which did you mean?`,
4228
+ options: options.map((o) => ({ value: o.value, label: o.label }))
4229
+ });
4230
+ if (isCancel(choice)) {
4231
+ process.exit(0);
4232
+ }
4233
+ return choice;
4234
+ }
4235
+ console.error(`Matched "${query}" \u2192 ${best.label}`);
4236
+ return best.value;
4237
+ }
4238
+ var isTTY2;
4239
+ var init_resolvers = __esm({
4240
+ "src/cli/resolvers.ts"() {
4241
+ "use strict";
4242
+ init_fuzzy();
4243
+ init_team_mapping();
4244
+ init_validation2();
4245
+ isTTY2 = process.stdout.isTTY === true;
4246
+ }
4247
+ });
4248
+
3788
4249
  // src/cli/commands/stats.ts
3789
4250
  var stats_exports = {};
3790
4251
  __export(stats_exports, {
@@ -3796,8 +4257,13 @@ var init_stats = __esm({
3796
4257
  "src/cli/commands/stats.ts"() {
3797
4258
  "use strict";
3798
4259
  init_index();
4260
+ init_fuzzy();
4261
+ init_afl_api();
4262
+ init_flags();
3799
4263
  init_formatters();
4264
+ init_resolvers();
3800
4265
  init_ui();
4266
+ init_validation2();
3801
4267
  DEFAULT_COLUMNS2 = [
3802
4268
  { key: "displayName", label: "Player", maxWidth: 22 },
3803
4269
  { key: "team", label: "Team", maxWidth: 18 },
@@ -3813,45 +4279,54 @@ var init_stats = __esm({
3813
4279
  description: "Fetch player statistics for a season"
3814
4280
  },
3815
4281
  args: {
3816
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
3817
- round: { type: "string", description: "Round number" },
3818
- "match-id": { type: "string", description: "Specific match ID" },
3819
- source: { type: "string", description: "Data source", default: "afl-api" },
3820
- competition: {
3821
- type: "string",
3822
- description: "Competition code (AFLM or AFLW)",
3823
- default: "AFLM"
3824
- },
3825
- json: { type: "boolean", description: "Output as JSON" },
3826
- csv: { type: "boolean", description: "Output as CSV" },
3827
- format: { type: "string", description: "Output format: table, json, csv" },
3828
- full: { type: "boolean", description: "Show all columns in table output" }
4282
+ ...SEASON_FLAG,
4283
+ ...ROUND_FLAG,
4284
+ match: { type: "string", description: "Filter by team name to find a specific match" },
4285
+ "match-id": { type: "string", description: "Specific match provider ID (advanced)" },
4286
+ ...SOURCE_FLAG,
4287
+ ...COMPETITION_FLAG,
4288
+ ...PLAYER_FLAG,
4289
+ ...OUTPUT_FLAGS
3829
4290
  },
3830
4291
  async run({ args }) {
3831
- const season = Number(args.season);
3832
- const round = args.round ? Number(args.round) : void 0;
3833
- const matchId = args["match-id"];
4292
+ const season = validateSeason(args.season);
4293
+ const round = args.round ? validateRound(args.round) : void 0;
4294
+ const source = validateSource(args.source);
4295
+ const competition = validateCompetition(args.competition);
4296
+ const format = validateFormat(args.format);
4297
+ let matchId = args["match-id"];
4298
+ if (!matchId && args.match && round != null) {
4299
+ const client = new AflApiClient();
4300
+ const seasonResult = await client.resolveCompSeason(competition, season);
4301
+ if (!seasonResult.success) throw seasonResult.error;
4302
+ const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, round);
4303
+ if (!itemsResult.success) throw itemsResult.error;
4304
+ matchId = await resolveMatchOrPrompt(args.match, itemsResult.data);
4305
+ } else if (args.match && round == null) {
4306
+ throw new Error("--match requires --round (-r) to identify which round to search.");
4307
+ }
3834
4308
  const result = await withSpinner(
3835
4309
  "Fetching player stats\u2026",
3836
- () => fetchPlayerStats({
3837
- source: args.source,
3838
- season,
3839
- round,
3840
- matchId,
3841
- competition: args.competition
3842
- })
4310
+ () => fetchPlayerStats({ source, season, round, matchId, competition })
3843
4311
  );
3844
4312
  if (!result.success) {
3845
4313
  throw result.error;
3846
4314
  }
3847
- const data = result.data;
4315
+ let data = result.data;
4316
+ if (args.player) {
4317
+ const playerMatches = fuzzySearch(args.player, data, (p) => p.displayName, {
4318
+ maxResults: 50,
4319
+ threshold: 0.4
4320
+ });
4321
+ data = playerMatches.map((m) => m.item);
4322
+ }
3848
4323
  showSummary(
3849
4324
  `Loaded ${data.length} player stat lines for ${season}${round ? ` round ${round}` : ""}`
3850
4325
  );
3851
4326
  const formatOptions = {
3852
4327
  json: args.json,
3853
4328
  csv: args.csv,
3854
- format: args.format,
4329
+ format,
3855
4330
  full: args.full,
3856
4331
  columns: DEFAULT_COLUMNS2
3857
4332
  };
@@ -3872,8 +4347,10 @@ var init_fixture2 = __esm({
3872
4347
  "src/cli/commands/fixture.ts"() {
3873
4348
  "use strict";
3874
4349
  init_index();
4350
+ init_flags();
3875
4351
  init_formatters();
3876
4352
  init_ui();
4353
+ init_validation2();
3877
4354
  DEFAULT_COLUMNS3 = [
3878
4355
  { key: "roundNumber", label: "Round", maxWidth: 6 },
3879
4356
  { key: "date", label: "Date", maxWidth: 16 },
@@ -3887,30 +4364,21 @@ var init_fixture2 = __esm({
3887
4364
  description: "Fetch fixture/schedule for a season"
3888
4365
  },
3889
4366
  args: {
3890
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
3891
- round: { type: "string", description: "Round number" },
3892
- source: { type: "string", description: "Data source", default: "afl-api" },
3893
- competition: {
3894
- type: "string",
3895
- description: "Competition code (AFLM or AFLW)",
3896
- default: "AFLM"
3897
- },
3898
- json: { type: "boolean", description: "Output as JSON" },
3899
- csv: { type: "boolean", description: "Output as CSV" },
3900
- format: { type: "string", description: "Output format: table, json, csv" },
3901
- full: { type: "boolean", description: "Show all columns in table output" }
4367
+ ...SEASON_FLAG,
4368
+ ...ROUND_FLAG,
4369
+ ...SOURCE_FLAG,
4370
+ ...COMPETITION_FLAG,
4371
+ ...OUTPUT_FLAGS
3902
4372
  },
3903
4373
  async run({ args }) {
3904
- const season = Number(args.season);
3905
- const round = args.round ? Number(args.round) : void 0;
4374
+ const season = validateSeason(args.season);
4375
+ const round = args.round ? validateRound(args.round) : void 0;
4376
+ const source = validateSource(args.source);
4377
+ const competition = validateCompetition(args.competition);
4378
+ const format = validateFormat(args.format);
3906
4379
  const result = await withSpinner(
3907
4380
  "Fetching fixture\u2026",
3908
- () => fetchFixture({
3909
- source: args.source,
3910
- season,
3911
- round,
3912
- competition: args.competition
3913
- })
4381
+ () => fetchFixture({ source, season, round, competition })
3914
4382
  );
3915
4383
  if (!result.success) {
3916
4384
  throw result.error;
@@ -3920,7 +4388,7 @@ var init_fixture2 = __esm({
3920
4388
  const formatOptions = {
3921
4389
  json: args.json,
3922
4390
  csv: args.csv,
3923
- format: args.format,
4391
+ format,
3924
4392
  full: args.full,
3925
4393
  columns: DEFAULT_COLUMNS3
3926
4394
  };
@@ -3941,8 +4409,10 @@ var init_ladder3 = __esm({
3941
4409
  "src/cli/commands/ladder.ts"() {
3942
4410
  "use strict";
3943
4411
  init_index();
4412
+ init_flags();
3944
4413
  init_formatters();
3945
4414
  init_ui();
4415
+ init_validation2();
3946
4416
  DEFAULT_COLUMNS4 = [
3947
4417
  { key: "position", label: "Pos", maxWidth: 4 },
3948
4418
  { key: "team", label: "Team", maxWidth: 24 },
@@ -3958,30 +4428,21 @@ var init_ladder3 = __esm({
3958
4428
  description: "Fetch ladder standings for a season"
3959
4429
  },
3960
4430
  args: {
3961
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
3962
- round: { type: "string", description: "Round number" },
3963
- source: { type: "string", description: "Data source", default: "afl-api" },
3964
- competition: {
3965
- type: "string",
3966
- description: "Competition code (AFLM or AFLW)",
3967
- default: "AFLM"
3968
- },
3969
- json: { type: "boolean", description: "Output as JSON" },
3970
- csv: { type: "boolean", description: "Output as CSV" },
3971
- format: { type: "string", description: "Output format: table, json, csv" },
3972
- full: { type: "boolean", description: "Show all columns in table output" }
4431
+ ...SEASON_FLAG,
4432
+ ...ROUND_FLAG,
4433
+ ...SOURCE_FLAG,
4434
+ ...COMPETITION_FLAG,
4435
+ ...OUTPUT_FLAGS
3973
4436
  },
3974
4437
  async run({ args }) {
3975
- const season = Number(args.season);
3976
- const round = args.round ? Number(args.round) : void 0;
4438
+ const season = validateSeason(args.season);
4439
+ const round = args.round ? validateRound(args.round) : void 0;
4440
+ const source = validateSource(args.source);
4441
+ const competition = validateCompetition(args.competition);
4442
+ const format = validateFormat(args.format);
3977
4443
  const result = await withSpinner(
3978
4444
  "Fetching ladder\u2026",
3979
- () => fetchLadder({
3980
- source: args.source,
3981
- season,
3982
- round,
3983
- competition: args.competition
3984
- })
4445
+ () => fetchLadder({ source, season, round, competition })
3985
4446
  );
3986
4447
  if (!result.success) {
3987
4448
  throw result.error;
@@ -3993,7 +4454,7 @@ var init_ladder3 = __esm({
3993
4454
  const formatOptions = {
3994
4455
  json: args.json,
3995
4456
  csv: args.csv,
3996
- format: args.format,
4457
+ format,
3997
4458
  full: args.full,
3998
4459
  columns: DEFAULT_COLUMNS4
3999
4460
  };
@@ -4009,17 +4470,45 @@ __export(lineup_exports, {
4009
4470
  lineupCommand: () => lineupCommand
4010
4471
  });
4011
4472
  import { defineCommand as defineCommand5 } from "citty";
4473
+ function flattenLineups(lineups) {
4474
+ const rows = [];
4475
+ for (const lineup of lineups) {
4476
+ for (const { players, team } of [
4477
+ { players: lineup.homePlayers, team: lineup.homeTeam },
4478
+ { players: lineup.awayPlayers, team: lineup.awayTeam }
4479
+ ]) {
4480
+ for (const p of players) {
4481
+ rows.push({
4482
+ matchId: lineup.matchId,
4483
+ team,
4484
+ displayName: p.displayName,
4485
+ jumperNumber: p.jumperNumber,
4486
+ position: p.position,
4487
+ isEmergency: p.isEmergency,
4488
+ isSubstitute: p.isSubstitute
4489
+ });
4490
+ }
4491
+ }
4492
+ }
4493
+ return rows;
4494
+ }
4012
4495
  var DEFAULT_COLUMNS5, lineupCommand;
4013
4496
  var init_lineup3 = __esm({
4014
4497
  "src/cli/commands/lineup.ts"() {
4015
4498
  "use strict";
4016
4499
  init_index();
4500
+ init_afl_api();
4501
+ init_flags();
4017
4502
  init_formatters();
4503
+ init_resolvers();
4018
4504
  init_ui();
4505
+ init_validation2();
4019
4506
  DEFAULT_COLUMNS5 = [
4020
- { key: "matchId", label: "Match", maxWidth: 12 },
4021
- { key: "homeTeam", label: "Home", maxWidth: 20 },
4022
- { key: "awayTeam", label: "Away", maxWidth: 20 }
4507
+ { key: "matchId", label: "Match", maxWidth: 14 },
4508
+ { key: "team", label: "Team", maxWidth: 20 },
4509
+ { key: "displayName", label: "Player", maxWidth: 24 },
4510
+ { key: "jumperNumber", label: "#", maxWidth: 4 },
4511
+ { key: "position", label: "Pos", maxWidth: 12 }
4023
4512
  ];
4024
4513
  lineupCommand = defineCommand5({
4025
4514
  meta: {
@@ -4027,33 +4516,32 @@ var init_lineup3 = __esm({
4027
4516
  description: "Fetch match lineups for a round"
4028
4517
  },
4029
4518
  args: {
4030
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4031
- round: { type: "string", description: "Round number", required: true },
4032
- "match-id": { type: "string", description: "Specific match ID" },
4033
- source: { type: "string", description: "Data source", default: "afl-api" },
4034
- competition: {
4035
- type: "string",
4036
- description: "Competition code (AFLM or AFLW)",
4037
- default: "AFLM"
4038
- },
4039
- json: { type: "boolean", description: "Output as JSON" },
4040
- csv: { type: "boolean", description: "Output as CSV" },
4041
- format: { type: "string", description: "Output format: table, json, csv" },
4042
- full: { type: "boolean", description: "Show all columns in table output" }
4519
+ ...SEASON_FLAG,
4520
+ ...REQUIRED_ROUND_FLAG,
4521
+ match: { type: "string", description: "Filter by team name to find a specific match" },
4522
+ "match-id": { type: "string", description: "Specific match provider ID (advanced)" },
4523
+ ...SOURCE_FLAG,
4524
+ ...COMPETITION_FLAG,
4525
+ ...OUTPUT_FLAGS
4043
4526
  },
4044
4527
  async run({ args }) {
4045
- const season = Number(args.season);
4046
- const round = Number(args.round);
4047
- const matchId = args["match-id"];
4528
+ const season = validateSeason(args.season);
4529
+ const round = validateRound(args.round);
4530
+ const source = validateSource(args.source);
4531
+ const competition = validateCompetition(args.competition);
4532
+ const format = validateFormat(args.format);
4533
+ let matchId = args["match-id"];
4534
+ if (!matchId && args.match) {
4535
+ const client = new AflApiClient();
4536
+ const seasonResult = await client.resolveCompSeason(competition, season);
4537
+ if (!seasonResult.success) throw seasonResult.error;
4538
+ const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, round);
4539
+ if (!itemsResult.success) throw itemsResult.error;
4540
+ matchId = await resolveMatchOrPrompt(args.match, itemsResult.data);
4541
+ }
4048
4542
  const result = await withSpinner(
4049
4543
  "Fetching lineups\u2026",
4050
- () => fetchLineup({
4051
- source: args.source,
4052
- season,
4053
- round,
4054
- matchId,
4055
- competition: args.competition
4056
- })
4544
+ () => fetchLineup({ source, season, round, matchId, competition })
4057
4545
  );
4058
4546
  if (!result.success) {
4059
4547
  throw result.error;
@@ -4063,11 +4551,16 @@ var init_lineup3 = __esm({
4063
4551
  const formatOptions = {
4064
4552
  json: args.json,
4065
4553
  csv: args.csv,
4066
- format: args.format,
4554
+ format,
4067
4555
  full: args.full,
4068
4556
  columns: DEFAULT_COLUMNS5
4069
4557
  };
4070
- console.log(formatOutput(data, formatOptions));
4558
+ const resolvedFormat = resolveFormat(formatOptions);
4559
+ if (resolvedFormat === "json") {
4560
+ console.log(formatOutput(data, formatOptions));
4561
+ } else {
4562
+ console.log(formatOutput(flattenLineups(data), formatOptions));
4563
+ }
4071
4564
  }
4072
4565
  });
4073
4566
  }
@@ -4084,8 +4577,11 @@ var init_squad = __esm({
4084
4577
  "src/cli/commands/squad.ts"() {
4085
4578
  "use strict";
4086
4579
  init_index();
4580
+ init_flags();
4087
4581
  init_formatters();
4582
+ init_resolvers();
4088
4583
  init_ui();
4584
+ init_validation2();
4089
4585
  DEFAULT_COLUMNS6 = [
4090
4586
  { key: "displayName", label: "Player", maxWidth: 24 },
4091
4587
  { key: "jumperNumber", label: "#", maxWidth: 4 },
@@ -4099,28 +4595,26 @@ var init_squad = __esm({
4099
4595
  description: "Fetch team squad for a season"
4100
4596
  },
4101
4597
  args: {
4102
- "team-id": { type: "string", description: "Team ID", required: true },
4103
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4104
- competition: {
4105
- type: "string",
4106
- description: "Competition code (AFLM or AFLW)",
4107
- default: "AFLM"
4108
- },
4109
- json: { type: "boolean", description: "Output as JSON" },
4110
- csv: { type: "boolean", description: "Output as CSV" },
4111
- format: { type: "string", description: "Output format: table, json, csv" },
4112
- full: { type: "boolean", description: "Show all columns in table output" }
4598
+ ...REQUIRED_TEAM_FLAG,
4599
+ ...SEASON_FLAG,
4600
+ ...COMPETITION_FLAG,
4601
+ ...OUTPUT_FLAGS
4113
4602
  },
4114
4603
  async run({ args }) {
4115
- const teamId = args["team-id"];
4116
- const season = Number(args.season);
4604
+ const season = validateSeason(args.season);
4605
+ const competition = validateCompetition(args.competition);
4606
+ const format = validateFormat(args.format);
4607
+ let teamId = args.team.trim();
4608
+ if (!/^\d+$/.test(teamId)) {
4609
+ const teamsResult = await withSpinner("Resolving team\u2026", () => fetchTeams({ competition }));
4610
+ if (!teamsResult.success) {
4611
+ throw teamsResult.error;
4612
+ }
4613
+ teamId = await resolveTeamOrPrompt(args.team, teamsResult.data);
4614
+ }
4117
4615
  const result = await withSpinner(
4118
4616
  "Fetching squad\u2026",
4119
- () => fetchSquad({
4120
- teamId,
4121
- season,
4122
- competition: args.competition
4123
- })
4617
+ () => fetchSquad({ teamId, season, competition })
4124
4618
  );
4125
4619
  if (!result.success) {
4126
4620
  throw result.error;
@@ -4130,7 +4624,7 @@ var init_squad = __esm({
4130
4624
  const formatOptions = {
4131
4625
  json: args.json,
4132
4626
  csv: args.csv,
4133
- format: args.format,
4627
+ format,
4134
4628
  full: args.full,
4135
4629
  columns: DEFAULT_COLUMNS6
4136
4630
  };
@@ -4151,8 +4645,10 @@ var init_teams2 = __esm({
4151
4645
  "src/cli/commands/teams.ts"() {
4152
4646
  "use strict";
4153
4647
  init_index();
4648
+ init_flags();
4154
4649
  init_formatters();
4155
4650
  init_ui();
4651
+ init_validation2();
4156
4652
  DEFAULT_COLUMNS7 = [
4157
4653
  { key: "teamId", label: "ID", maxWidth: 8 },
4158
4654
  { key: "name", label: "Team", maxWidth: 24 },
@@ -4165,30 +4661,31 @@ var init_teams2 = __esm({
4165
4661
  description: "Fetch team list"
4166
4662
  },
4167
4663
  args: {
4168
- competition: { type: "string", description: "Competition code (AFLM or AFLW)" },
4664
+ ...OPTIONAL_COMPETITION_FLAG,
4169
4665
  "team-type": { type: "string", description: "Team type filter" },
4170
- json: { type: "boolean", description: "Output as JSON" },
4171
- csv: { type: "boolean", description: "Output as CSV" },
4172
- format: { type: "string", description: "Output format: table, json, csv" },
4173
- full: { type: "boolean", description: "Show all columns in table output" }
4666
+ ...OUTPUT_FLAGS
4174
4667
  },
4175
4668
  async run({ args }) {
4669
+ const competition = validateOptionalCompetition(args.competition);
4670
+ const format = validateFormat(args.format);
4176
4671
  const result = await withSpinner(
4177
4672
  "Fetching teams\u2026",
4178
- () => fetchTeams({
4179
- competition: args.competition,
4180
- teamType: args["team-type"]
4181
- })
4673
+ () => fetchTeams({ competition, teamType: args["team-type"] })
4182
4674
  );
4183
4675
  if (!result.success) {
4184
4676
  throw result.error;
4185
4677
  }
4186
4678
  const data = result.data;
4679
+ if (data.length === 0 && args["team-type"]) {
4680
+ console.error(
4681
+ `No teams found for team type "${args["team-type"]}". Try running without --team-type to see available teams.`
4682
+ );
4683
+ }
4187
4684
  showSummary(`Loaded ${data.length} teams`);
4188
4685
  const formatOptions = {
4189
4686
  json: args.json,
4190
4687
  csv: args.csv,
4191
- format: args.format,
4688
+ format,
4192
4689
  full: args.full,
4193
4690
  columns: DEFAULT_COLUMNS7
4194
4691
  };
@@ -4215,11 +4712,21 @@ var init_team_stats2 = __esm({
4215
4712
  "src/cli/commands/team-stats.ts"() {
4216
4713
  "use strict";
4217
4714
  init_index();
4715
+ init_flags();
4218
4716
  init_formatters();
4219
4717
  init_ui();
4718
+ init_validation2();
4220
4719
  DEFAULT_COLUMNS8 = [
4221
4720
  { key: "team", label: "Team", maxWidth: 24 },
4222
- { key: "gamesPlayed", label: "GP", maxWidth: 5 }
4721
+ { key: "gamesPlayed", label: "GP", maxWidth: 5 },
4722
+ { key: "K", label: "K", maxWidth: 6 },
4723
+ { key: "HB", label: "HB", maxWidth: 6 },
4724
+ { key: "D", label: "D", maxWidth: 6 },
4725
+ { key: "M", label: "M", maxWidth: 6 },
4726
+ { key: "G", label: "G", maxWidth: 6 },
4727
+ { key: "B", label: "B", maxWidth: 6 },
4728
+ { key: "T", label: "T", maxWidth: 6 },
4729
+ { key: "I50", label: "I50", maxWidth: 6 }
4223
4730
  ];
4224
4731
  teamStatsCommand = defineCommand8({
4225
4732
  meta: {
@@ -4227,28 +4734,23 @@ var init_team_stats2 = __esm({
4227
4734
  description: "Fetch team aggregate statistics for a season"
4228
4735
  },
4229
4736
  args: {
4230
- season: { type: "string", description: "Season year (e.g. 2024)", required: true },
4737
+ ...SEASON_FLAG,
4231
4738
  source: {
4232
4739
  type: "string",
4233
4740
  description: "Data source (footywire, afl-tables)",
4234
4741
  default: "footywire"
4235
4742
  },
4236
4743
  summary: { type: "string", description: "Summary type: totals or averages", default: "totals" },
4237
- json: { type: "boolean", description: "Output as JSON" },
4238
- csv: { type: "boolean", description: "Output as CSV" },
4239
- format: { type: "string", description: "Output format: table, json, csv" },
4240
- full: { type: "boolean", description: "Show all columns in table output" }
4744
+ ...OUTPUT_FLAGS
4241
4745
  },
4242
4746
  async run({ args }) {
4243
- const season = Number(args.season);
4747
+ const season = validateSeason(args.season);
4748
+ const source = validateSource(args.source);
4749
+ const format = validateFormat(args.format);
4244
4750
  const summaryType = args.summary;
4245
4751
  const result = await withSpinner(
4246
4752
  "Fetching team stats\u2026",
4247
- () => fetchTeamStats({
4248
- source: args.source,
4249
- season,
4250
- summaryType
4251
- })
4753
+ () => fetchTeamStats({ source, season, summaryType })
4252
4754
  );
4253
4755
  if (!result.success) {
4254
4756
  throw result.error;
@@ -4259,7 +4761,7 @@ var init_team_stats2 = __esm({
4259
4761
  const formatOptions = {
4260
4762
  json: args.json,
4261
4763
  csv: args.csv,
4262
- format: args.format,
4764
+ format,
4263
4765
  full: args.full,
4264
4766
  columns: DEFAULT_COLUMNS8
4265
4767
  };
@@ -4280,8 +4782,11 @@ var init_player_details2 = __esm({
4280
4782
  "src/cli/commands/player-details.ts"() {
4281
4783
  "use strict";
4282
4784
  init_index();
4785
+ init_flags();
4283
4786
  init_formatters();
4787
+ init_resolvers();
4284
4788
  init_ui();
4789
+ init_validation2();
4285
4790
  DEFAULT_COLUMNS9 = [
4286
4791
  { key: "displayName", label: "Player", maxWidth: 24 },
4287
4792
  { key: "jumperNumber", label: "#", maxWidth: 4 },
@@ -4297,44 +4802,35 @@ var init_player_details2 = __esm({
4297
4802
  description: "Fetch player biographical details for a team"
4298
4803
  },
4299
4804
  args: {
4300
- team: { type: "positional", description: "Team name (e.g. Carlton, Hawthorn)", required: true },
4805
+ ...REQUIRED_TEAM_FLAG,
4301
4806
  source: {
4302
4807
  type: "string",
4303
4808
  description: "Data source: afl-api, footywire, afl-tables",
4304
4809
  default: "afl-api"
4305
4810
  },
4306
- season: { type: "string", description: "Season year (for AFL API source, e.g. 2025)" },
4307
- competition: {
4308
- type: "string",
4309
- description: "Competition code (AFLM or AFLW)",
4310
- default: "AFLM"
4311
- },
4312
- json: { type: "boolean", description: "Output as JSON" },
4313
- csv: { type: "boolean", description: "Output as CSV" },
4314
- format: { type: "string", description: "Output format: table, json, csv" },
4315
- full: { type: "boolean", description: "Show all columns in table output" }
4811
+ ...OPTIONAL_SEASON_FLAG,
4812
+ ...COMPETITION_FLAG,
4813
+ ...OUTPUT_FLAGS
4316
4814
  },
4317
4815
  async run({ args }) {
4318
- const source = args.source;
4319
- const season = args.season ? Number(args.season) : void 0;
4816
+ const source = validateSource(args.source);
4817
+ const competition = validateCompetition(args.competition);
4818
+ const format = validateFormat(args.format);
4819
+ const season = validateOptionalSeason(args.season) ?? resolveDefaultSeason(competition);
4820
+ const team = await resolveTeamNameOrPrompt(args.team);
4320
4821
  const result = await withSpinner(
4321
4822
  "Fetching player details\u2026",
4322
- () => fetchPlayerDetails({
4323
- source,
4324
- team: args.team,
4325
- season,
4326
- competition: args.competition
4327
- })
4823
+ () => fetchPlayerDetails({ source, team, season, competition })
4328
4824
  );
4329
4825
  if (!result.success) {
4330
4826
  throw result.error;
4331
4827
  }
4332
4828
  const data = result.data;
4333
- showSummary(`Loaded ${data.length} players for ${args.team} (${source})`);
4829
+ showSummary(`Loaded ${data.length} players for ${team} (${source})`);
4334
4830
  const formatOptions = {
4335
4831
  json: args.json,
4336
4832
  csv: args.csv,
4337
- format: args.format,
4833
+ format,
4338
4834
  full: args.full,
4339
4835
  columns: DEFAULT_COLUMNS9
4340
4836
  };
@@ -4355,8 +4851,11 @@ var init_coaches_votes2 = __esm({
4355
4851
  "src/cli/commands/coaches-votes.ts"() {
4356
4852
  "use strict";
4357
4853
  init_index();
4854
+ init_flags();
4358
4855
  init_formatters();
4856
+ init_resolvers();
4359
4857
  init_ui();
4858
+ init_validation2();
4360
4859
  DEFAULT_COLUMNS10 = [
4361
4860
  { key: "season", label: "Season", maxWidth: 8 },
4362
4861
  { key: "round", label: "Round", maxWidth: 6 },
@@ -4371,42 +4870,33 @@ var init_coaches_votes2 = __esm({
4371
4870
  description: "Fetch AFLCA coaches votes for a season"
4372
4871
  },
4373
4872
  args: {
4374
- season: { type: "string", description: "Season year (e.g. 2024)", required: true },
4375
- round: { type: "string", description: "Round number" },
4376
- competition: {
4377
- type: "string",
4378
- description: "Competition code (AFLM or AFLW)",
4379
- default: "AFLM"
4380
- },
4381
- team: { type: "string", description: "Filter by team name" },
4382
- json: { type: "boolean", description: "Output as JSON" },
4383
- csv: { type: "boolean", description: "Output as CSV" },
4384
- format: { type: "string", description: "Output format: table, json, csv" },
4385
- full: { type: "boolean", description: "Show all columns in table output" }
4873
+ ...SEASON_FLAG,
4874
+ ...ROUND_FLAG,
4875
+ ...COMPETITION_FLAG,
4876
+ ...TEAM_FLAG,
4877
+ ...OUTPUT_FLAGS
4386
4878
  },
4387
4879
  async run({ args }) {
4388
- const season = Number(args.season);
4389
- const round = args.round ? Number(args.round) : void 0;
4880
+ const season = validateSeason(args.season);
4881
+ const round = args.round ? validateRound(args.round) : void 0;
4882
+ const competition = validateCompetition(args.competition);
4883
+ const format = validateFormat(args.format);
4884
+ const team = args.team ? await resolveTeamNameOrPrompt(args.team) : void 0;
4390
4885
  const result = await withSpinner(
4391
4886
  "Fetching coaches votes\u2026",
4392
- () => fetchCoachesVotes({
4393
- season,
4394
- round,
4395
- competition: args.competition,
4396
- team: args.team
4397
- })
4887
+ () => fetchCoachesVotes({ season, round, competition, team })
4398
4888
  );
4399
4889
  if (!result.success) {
4400
4890
  throw result.error;
4401
4891
  }
4402
4892
  const data = result.data;
4403
- const teamSuffix = args.team ? ` for ${args.team}` : "";
4893
+ const teamSuffix = team ? ` for ${team}` : "";
4404
4894
  const roundSuffix = round ? ` round ${round}` : "";
4405
4895
  showSummary(`Loaded ${data.length} vote records for ${season}${roundSuffix}${teamSuffix}`);
4406
4896
  const formatOptions = {
4407
4897
  json: args.json,
4408
4898
  csv: args.csv,
4409
- format: args.format,
4899
+ format,
4410
4900
  full: args.full,
4411
4901
  columns: DEFAULT_COLUMNS10
4412
4902
  };
@@ -4417,13 +4907,39 @@ var init_coaches_votes2 = __esm({
4417
4907
  });
4418
4908
 
4419
4909
  // src/cli.ts
4420
- init_errors();
4421
4910
  import { defineCommand as defineCommand11, runMain } from "citty";
4422
- import pc2 from "picocolors";
4911
+
4912
+ // src/cli/error-boundary.ts
4913
+ init_errors();
4914
+ import pc from "picocolors";
4915
+ function formatError(error) {
4916
+ if (error instanceof ValidationError && error.issues) {
4917
+ const issueLines = error.issues.map((i) => ` ${pc.yellow(i.path)}: ${i.message}`);
4918
+ return `${pc.red("Validation error:")}
4919
+ ${issueLines.join("\n")}`;
4920
+ }
4921
+ if (error instanceof AflApiError) {
4922
+ const status = error.statusCode ? ` (HTTP ${error.statusCode})` : "";
4923
+ return `${pc.red("AFL API error:")} ${error.message}${status}`;
4924
+ }
4925
+ if (error instanceof ScrapeError) {
4926
+ const source = error.source ? ` [${error.source}]` : "";
4927
+ return `${pc.red("Scrape error:")} ${error.message}${source}`;
4928
+ }
4929
+ if (error instanceof UnsupportedSourceError) {
4930
+ return `${pc.red("Unsupported source:")} ${error.message}`;
4931
+ }
4932
+ if (error instanceof Error) {
4933
+ return `${pc.red("Error:")} ${error.message}`;
4934
+ }
4935
+ return `${pc.red("Error:")} ${String(error)}`;
4936
+ }
4937
+
4938
+ // src/cli.ts
4423
4939
  var main = defineCommand11({
4424
4940
  meta: {
4425
4941
  name: "fitzroy",
4426
- version: "1.1.0",
4942
+ version: "1.2.0",
4427
4943
  description: "CLI for fetching AFL data \u2014 match results, player stats, fixtures, ladders, and more"
4428
4944
  },
4429
4945
  subCommands: {
@@ -4439,28 +4955,6 @@ var main = defineCommand11({
4439
4955
  "coaches-votes": () => Promise.resolve().then(() => (init_coaches_votes2(), coaches_votes_exports)).then((m) => m.coachesVotesCommand)
4440
4956
  }
4441
4957
  });
4442
- function formatError(error) {
4443
- if (error instanceof ValidationError && error.issues) {
4444
- const issueLines = error.issues.map((i) => ` ${pc2.yellow(i.path)}: ${i.message}`);
4445
- return `${pc2.red("Validation error:")}
4446
- ${issueLines.join("\n")}`;
4447
- }
4448
- if (error instanceof AflApiError) {
4449
- const status = error.statusCode ? ` (HTTP ${error.statusCode})` : "";
4450
- return `${pc2.red("AFL API error:")} ${error.message}${status}`;
4451
- }
4452
- if (error instanceof ScrapeError) {
4453
- const source = error.source ? ` [${error.source}]` : "";
4454
- return `${pc2.red("Scrape error:")} ${error.message}${source}`;
4455
- }
4456
- if (error instanceof UnsupportedSourceError) {
4457
- return `${pc2.red("Unsupported source:")} ${error.message}`;
4458
- }
4459
- if (error instanceof Error) {
4460
- return `${pc2.red("Error:")} ${error.message}`;
4461
- }
4462
- return `${pc2.red("Error:")} ${String(error)}`;
4463
- }
4464
4958
  runMain(main).catch((error) => {
4465
4959
  console.error(formatError(error));
4466
4960
  process.exit(1);