fitzroy 1.1.1 → 1.3.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
@@ -10,6 +10,12 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/lib/errors.ts
13
+ function aflwUnsupportedError(source) {
14
+ return new UnsupportedSourceError(
15
+ `AFLW data is not available from ${source}. Use --source afl-api for AFLW data.`,
16
+ source
17
+ );
18
+ }
13
19
  var AflApiError, ScrapeError, UnsupportedSourceError, ValidationError;
14
20
  var init_errors = __esm({
15
21
  "src/lib/errors.ts"() {
@@ -45,6 +51,47 @@ var init_errors = __esm({
45
51
  }
46
52
  });
47
53
 
54
+ // src/cli/error-boundary.ts
55
+ import pc from "picocolors";
56
+ function formatError(error) {
57
+ if (error instanceof ValidationError && error.issues) {
58
+ const issueLines = error.issues.map((i) => ` ${pc.yellow(i.path)}: ${i.message}`);
59
+ return `${pc.red("Validation error:")}
60
+ ${issueLines.join("\n")}`;
61
+ }
62
+ if (error instanceof AflApiError) {
63
+ const status = error.statusCode ? ` (HTTP ${error.statusCode})` : "";
64
+ return `${pc.red("AFL API error:")} ${error.message}${status}`;
65
+ }
66
+ if (error instanceof ScrapeError) {
67
+ const source = error.source ? ` [${error.source}]` : "";
68
+ return `${pc.red("Scrape error:")} ${error.message}${source}`;
69
+ }
70
+ if (error instanceof UnsupportedSourceError) {
71
+ return `${pc.red("Unsupported source:")} ${error.message}`;
72
+ }
73
+ if (error instanceof Error) {
74
+ return `${pc.red("Error:")} ${error.message}`;
75
+ }
76
+ return `${pc.red("Error:")} ${String(error)}`;
77
+ }
78
+ function withErrorBoundary(fn) {
79
+ return async (ctx) => {
80
+ try {
81
+ await fn(ctx);
82
+ } catch (error) {
83
+ console.error(formatError(error));
84
+ process.exit(1);
85
+ }
86
+ };
87
+ }
88
+ var init_error_boundary = __esm({
89
+ "src/cli/error-boundary.ts"() {
90
+ "use strict";
91
+ init_errors();
92
+ }
93
+ });
94
+
48
95
  // src/lib/result.ts
49
96
  function ok(data) {
50
97
  return { success: true, data };
@@ -117,6 +164,10 @@ function parseAflTablesDate(dateStr) {
117
164
  }
118
165
  return null;
119
166
  }
167
+ function resolveDefaultSeason(competition = "AFLM") {
168
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
169
+ return competition === "AFLW" ? year - 1 : year;
170
+ }
120
171
  function buildUtcDate(year, monthStr, day) {
121
172
  const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
122
173
  if (monthIndex === void 0) {
@@ -168,7 +219,7 @@ function normaliseTeamName(raw) {
168
219
  const trimmed = raw.trim();
169
220
  return ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
170
221
  }
171
- var TEAM_ALIASES, AFL_SENIOR_TEAMS, ALIAS_MAP;
222
+ var TEAM_ALIASES, AFL_SENIOR_TEAMS, ALIAS_MAP, AFL_API_TEAM_IDS;
172
223
  var init_team_mapping = __esm({
173
224
  "src/lib/team-mapping.ts"() {
174
225
  "use strict";
@@ -241,6 +292,26 @@ var init_team_mapping = __esm({
241
292
  }
242
293
  return map;
243
294
  })();
295
+ AFL_API_TEAM_IDS = /* @__PURE__ */ new Map([
296
+ ["CD_T10", "Adelaide Crows"],
297
+ ["CD_T20", "Brisbane Lions"],
298
+ ["CD_T30", "Carlton"],
299
+ ["CD_T40", "Collingwood"],
300
+ ["CD_T50", "Essendon"],
301
+ ["CD_T60", "Fremantle"],
302
+ ["CD_T70", "Geelong Cats"],
303
+ ["CD_T1000", "Gold Coast Suns"],
304
+ ["CD_T1010", "GWS Giants"],
305
+ ["CD_T80", "Hawthorn"],
306
+ ["CD_T90", "Melbourne"],
307
+ ["CD_T100", "North Melbourne"],
308
+ ["CD_T110", "Port Adelaide"],
309
+ ["CD_T120", "Richmond"],
310
+ ["CD_T130", "St Kilda"],
311
+ ["CD_T160", "Sydney Swans"],
312
+ ["CD_T150", "West Coast Eagles"],
313
+ ["CD_T140", "Western Bulldogs"]
314
+ ]);
244
315
  }
245
316
  });
246
317
 
@@ -506,6 +577,14 @@ var init_footywire_player_stats = __esm({
506
577
  function inferRoundType(roundName) {
507
578
  return FINALS_PATTERN.test(roundName) ? "Finals" : "HomeAndAway";
508
579
  }
580
+ function finalsRoundNumber(headerText, lastHARound) {
581
+ const lower = headerText.toLowerCase();
582
+ if (lower.includes("qualifying") || lower.includes("elimination")) return lastHARound + 1;
583
+ if (lower.includes("semi")) return lastHARound + 2;
584
+ if (lower.includes("preliminary")) return lastHARound + 3;
585
+ if (lower.includes("grand")) return lastHARound + 4;
586
+ return lastHARound + 1;
587
+ }
509
588
  function toMatchStatus(raw) {
510
589
  switch (raw) {
511
590
  case "CONCLUDED":
@@ -595,6 +674,7 @@ function parseMatchList(html, year) {
595
674
  const $ = cheerio2.load(html);
596
675
  const results = [];
597
676
  let currentRound = 0;
677
+ let lastHARound = 0;
598
678
  let currentRoundType = "HomeAndAway";
599
679
  $("tr").each((_i, row) => {
600
680
  const roundHeader = $(row).find("td[colspan='7']");
@@ -604,6 +684,11 @@ function parseMatchList(html, year) {
604
684
  const roundMatch = /Round\s+(\d+)/i.exec(text);
605
685
  if (roundMatch?.[1]) {
606
686
  currentRound = Number.parseInt(roundMatch[1], 10);
687
+ if (currentRoundType === "HomeAndAway") {
688
+ lastHARound = currentRound;
689
+ }
690
+ } else if (currentRoundType === "Finals") {
691
+ currentRound = finalsRoundNumber(text, lastHARound);
607
692
  }
608
693
  return;
609
694
  }
@@ -674,6 +759,7 @@ function parseFixtureList(html, year) {
674
759
  const $ = cheerio2.load(html);
675
760
  const fixtures = [];
676
761
  let currentRound = 0;
762
+ let lastHARound = 0;
677
763
  let currentRoundType = "HomeAndAway";
678
764
  let gameNumber = 0;
679
765
  $("tr").each((_i, row) => {
@@ -684,6 +770,11 @@ function parseFixtureList(html, year) {
684
770
  const roundMatch = /Round\s+(\d+)/i.exec(text);
685
771
  if (roundMatch?.[1]) {
686
772
  currentRound = Number.parseInt(roundMatch[1], 10);
773
+ if (currentRoundType === "HomeAndAway") {
774
+ lastHARound = currentRound;
775
+ }
776
+ } else if (currentRoundType === "Finals") {
777
+ currentRound = finalsRoundNumber(text, lastHARound);
687
778
  }
688
779
  return;
689
780
  }
@@ -1301,6 +1392,27 @@ var init_coaches_votes = __esm({
1301
1392
  }
1302
1393
  });
1303
1394
 
1395
+ // src/lib/concurrency.ts
1396
+ async function batchedMap(items, fn, options) {
1397
+ const batchSize = options?.batchSize ?? 5;
1398
+ const delayMs = options?.delayMs ?? 0;
1399
+ const results = [];
1400
+ for (let i = 0; i < items.length; i += batchSize) {
1401
+ const batch = items.slice(i, i + batchSize);
1402
+ const batchResults = await Promise.all(batch.map(fn));
1403
+ results.push(...batchResults);
1404
+ if (delayMs > 0 && i + batchSize < items.length) {
1405
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1406
+ }
1407
+ }
1408
+ return results;
1409
+ }
1410
+ var init_concurrency = __esm({
1411
+ "src/lib/concurrency.ts"() {
1412
+ "use strict";
1413
+ }
1414
+ });
1415
+
1304
1416
  // src/lib/validation.ts
1305
1417
  import { z } from "zod/v4";
1306
1418
  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;
@@ -1410,7 +1522,14 @@ var init_validation = __esm({
1410
1522
  captain: z.boolean().optional(),
1411
1523
  playerJumperNumber: z.number().optional()
1412
1524
  }).passthrough();
1413
- statNum = z.number().nullable().optional();
1525
+ statNum = z.union([
1526
+ z.number(),
1527
+ z.string().transform((s) => {
1528
+ if (s === "" || s === "-") return null;
1529
+ const n = Number(s);
1530
+ return Number.isNaN(n) ? null : n;
1531
+ })
1532
+ ]).nullable().optional();
1414
1533
  PlayerGameStatsSchema = z.object({
1415
1534
  goals: statNum,
1416
1535
  behinds: statNum,
@@ -1488,8 +1607,8 @@ var init_validation = __esm({
1488
1607
  teamId: z.string(),
1489
1608
  playerStats: z.object({
1490
1609
  stats: PlayerGameStatsSchema,
1491
- timeOnGroundPercentage: z.number().nullable().optional()
1492
- }).passthrough()
1610
+ timeOnGroundPercentage: statNum
1611
+ }).passthrough().nullable().optional()
1493
1612
  }).passthrough();
1494
1613
  PlayerStatsListSchema = z.object({
1495
1614
  homeTeamPlayerStats: z.array(PlayerStatsItemSchema),
@@ -1589,6 +1708,7 @@ var TOKEN_URL, API_BASE, CFS_BASE, AflApiClient;
1589
1708
  var init_afl_api = __esm({
1590
1709
  "src/sources/afl-api.ts"() {
1591
1710
  "use strict";
1711
+ init_concurrency();
1592
1712
  init_errors();
1593
1713
  init_result();
1594
1714
  init_validation();
@@ -1599,6 +1719,7 @@ var init_afl_api = __esm({
1599
1719
  fetchFn;
1600
1720
  tokenUrl;
1601
1721
  cachedToken = null;
1722
+ pendingAuth = null;
1602
1723
  constructor(options) {
1603
1724
  this.fetchFn = options?.fetchFn ?? globalThis.fetch;
1604
1725
  this.tokenUrl = options?.tokenUrl ?? TOKEN_URL;
@@ -1606,9 +1727,21 @@ var init_afl_api = __esm({
1606
1727
  /**
1607
1728
  * Authenticate with the WMCTok token endpoint and cache the token.
1608
1729
  *
1730
+ * Concurrent callers share the same in-flight request to avoid
1731
+ * redundant token fetches (thundering herd prevention).
1732
+ *
1609
1733
  * @returns The access token on success, or an error Result.
1610
1734
  */
1611
1735
  async authenticate() {
1736
+ if (this.pendingAuth) {
1737
+ return this.pendingAuth;
1738
+ }
1739
+ this.pendingAuth = this.doAuthenticate().finally(() => {
1740
+ this.pendingAuth = null;
1741
+ });
1742
+ return this.pendingAuth;
1743
+ }
1744
+ async doAuthenticate() {
1612
1745
  try {
1613
1746
  const response = await this.fetchFn(this.tokenUrl, {
1614
1747
  method: "POST",
@@ -1863,7 +1996,7 @@ var init_afl_api = __esm({
1863
1996
  return roundsResult;
1864
1997
  }
1865
1998
  const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
1866
- const results = await Promise.all(providerIds.map((id) => this.fetchRoundMatchItems(id)));
1999
+ const results = await batchedMap(providerIds, (id) => this.fetchRoundMatchItems(id));
1867
2000
  const allItems = [];
1868
2001
  for (const result of results) {
1869
2002
  if (!result.success) {
@@ -2186,12 +2319,14 @@ function toFixture(item, season, fallbackRoundNumber, competition) {
2186
2319
  async function fetchFixture(query) {
2187
2320
  const competition = query.competition ?? "AFLM";
2188
2321
  if (query.source === "squiggle") {
2322
+ if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
2189
2323
  const client2 = new SquiggleClient();
2190
2324
  const result = await client2.fetchGames(query.season, query.round ?? void 0);
2191
2325
  if (!result.success) return result;
2192
2326
  return ok(transformSquiggleGamesToFixture(result.data.games, query.season));
2193
2327
  }
2194
2328
  if (query.source === "footywire") {
2329
+ if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
2195
2330
  const fwClient = new FootyWireClient();
2196
2331
  const result = await fwClient.fetchSeasonFixture(query.season);
2197
2332
  if (!result.success) return result;
@@ -2221,8 +2356,9 @@ async function fetchFixture(query) {
2221
2356
  const roundProviderIds = roundsResult.data.flatMap(
2222
2357
  (r) => r.providerId ? [{ providerId: r.providerId, roundNumber: r.roundNumber }] : []
2223
2358
  );
2224
- const roundResults = await Promise.all(
2225
- roundProviderIds.map((r) => client.fetchRoundMatchItems(r.providerId))
2359
+ const roundResults = await batchedMap(
2360
+ roundProviderIds,
2361
+ (r) => client.fetchRoundMatchItems(r.providerId)
2226
2362
  );
2227
2363
  const fixtures = [];
2228
2364
  for (let i = 0; i < roundResults.length; i++) {
@@ -2238,6 +2374,7 @@ async function fetchFixture(query) {
2238
2374
  var init_fixture = __esm({
2239
2375
  "src/api/fixture.ts"() {
2240
2376
  "use strict";
2377
+ init_concurrency();
2241
2378
  init_errors();
2242
2379
  init_result();
2243
2380
  init_team_mapping();
@@ -2380,6 +2517,7 @@ function parseSeasonPage(html, year) {
2380
2517
  const results = [];
2381
2518
  let currentRound = 0;
2382
2519
  let currentRoundType = "HomeAndAway";
2520
+ let lastHARound = 0;
2383
2521
  let matchCounter = 0;
2384
2522
  $("table").each((_i, table) => {
2385
2523
  const $table = $(table);
@@ -2389,10 +2527,14 @@ function parseSeasonPage(html, year) {
2389
2527
  if (roundMatch?.[1] && border !== "1") {
2390
2528
  currentRound = Number.parseInt(roundMatch[1], 10);
2391
2529
  currentRoundType = inferRoundType(text);
2530
+ if (currentRoundType === "HomeAndAway") {
2531
+ lastHARound = currentRound;
2532
+ }
2392
2533
  return;
2393
2534
  }
2394
2535
  if (border !== "1" && inferRoundType(text) === "Finals") {
2395
2536
  currentRoundType = "Finals";
2537
+ currentRound = finalsRoundNumber(text, lastHARound);
2396
2538
  return;
2397
2539
  }
2398
2540
  if (border !== "1") return;
@@ -2902,6 +3044,7 @@ var init_ladder = __esm({
2902
3044
  async function fetchLadder(query) {
2903
3045
  const competition = query.competition ?? "AFLM";
2904
3046
  if (query.source === "squiggle") {
3047
+ if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
2905
3048
  const client2 = new SquiggleClient();
2906
3049
  const result = await client2.fetchStandings(query.season, query.round ?? void 0);
2907
3050
  if (!result.success) return result;
@@ -2913,6 +3056,7 @@ async function fetchLadder(query) {
2913
3056
  });
2914
3057
  }
2915
3058
  if (query.source === "afl-tables") {
3059
+ if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
2916
3060
  const atClient = new AflTablesClient();
2917
3061
  const resultsResult = await atClient.fetchSeasonResults(query.season);
2918
3062
  if (!resultsResult.success) return resultsResult;
@@ -3034,8 +3178,9 @@ async function fetchLineup(query) {
3034
3178
  if (matchItems.data.length === 0) {
3035
3179
  return err(new AflApiError(`No matches found for round ${query.round}`));
3036
3180
  }
3037
- const rosterResults = await Promise.all(
3038
- matchItems.data.map((item) => client.fetchMatchRoster(item.match.matchId))
3181
+ const rosterResults = await batchedMap(
3182
+ matchItems.data,
3183
+ (item) => client.fetchMatchRoster(item.match.matchId)
3039
3184
  );
3040
3185
  const lineups = [];
3041
3186
  for (const rosterResult of rosterResults) {
@@ -3047,6 +3192,7 @@ async function fetchLineup(query) {
3047
3192
  var init_lineup2 = __esm({
3048
3193
  "src/api/lineup.ts"() {
3049
3194
  "use strict";
3195
+ init_concurrency();
3050
3196
  init_errors();
3051
3197
  init_result();
3052
3198
  init_afl_api();
@@ -3075,6 +3221,7 @@ async function fetchMatchResults(query) {
3075
3221
  return ok(transformMatchItems(itemsResult.data, query.season, competition));
3076
3222
  }
3077
3223
  case "footywire": {
3224
+ if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
3078
3225
  const client = new FootyWireClient();
3079
3226
  const result = await client.fetchSeasonResults(query.season);
3080
3227
  if (!result.success) return result;
@@ -3084,6 +3231,7 @@ async function fetchMatchResults(query) {
3084
3231
  return result;
3085
3232
  }
3086
3233
  case "afl-tables": {
3234
+ if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
3087
3235
  const client = new AflTablesClient();
3088
3236
  const result = await client.fetchSeasonResults(query.season);
3089
3237
  if (!result.success) return result;
@@ -3093,6 +3241,7 @@ async function fetchMatchResults(query) {
3093
3241
  return result;
3094
3242
  }
3095
3243
  case "squiggle": {
3244
+ if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
3096
3245
  const client = new SquiggleClient();
3097
3246
  const result = await client.fetchGames(query.season, query.round ?? void 0, 100);
3098
3247
  if (!result.success) return result;
@@ -3131,7 +3280,7 @@ async function resolveTeamId(client, teamName, competition) {
3131
3280
  async function fetchFromAflApi(query) {
3132
3281
  const client = new AflApiClient();
3133
3282
  const competition = query.competition ?? "AFLM";
3134
- const season = query.season ?? (/* @__PURE__ */ new Date()).getFullYear();
3283
+ const season = query.season ?? resolveDefaultSeason(competition);
3135
3284
  const [teamIdResult, seasonResult] = await Promise.all([
3136
3285
  resolveTeamId(client, query.team, competition),
3137
3286
  client.resolveCompSeason(competition, season)
@@ -3154,8 +3303,8 @@ async function fetchFromAflApi(query) {
3154
3303
  jumperNumber: p.jumperNumber ?? null,
3155
3304
  position: p.position ?? null,
3156
3305
  dateOfBirth: p.player.dateOfBirth ?? null,
3157
- heightCm: p.player.heightInCm ?? null,
3158
- weightKg: p.player.weightInKg ?? null,
3306
+ heightCm: p.player.heightInCm || null,
3307
+ weightKg: p.player.weightInKg || null,
3159
3308
  gamesPlayed: null,
3160
3309
  goals: null,
3161
3310
  draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
@@ -3169,8 +3318,9 @@ async function fetchFromAflApi(query) {
3169
3318
  return ok(players);
3170
3319
  }
3171
3320
  async function fetchFromFootyWire(query) {
3172
- const client = new FootyWireClient();
3173
3321
  const competition = query.competition ?? "AFLM";
3322
+ if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
3323
+ const client = new FootyWireClient();
3174
3324
  const teamName = normaliseTeamName(query.team);
3175
3325
  const result = await client.fetchPlayerList(teamName);
3176
3326
  if (!result.success) return result;
@@ -3182,8 +3332,9 @@ async function fetchFromFootyWire(query) {
3182
3332
  return ok(players);
3183
3333
  }
3184
3334
  async function fetchFromAflTables(query) {
3185
- const client = new AflTablesClient();
3186
3335
  const competition = query.competition ?? "AFLM";
3336
+ if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
3337
+ const client = new AflTablesClient();
3187
3338
  const teamName = normaliseTeamName(query.team);
3188
3339
  const result = await client.fetchPlayerList(teamName);
3189
3340
  if (!result.success) return result;
@@ -3214,6 +3365,7 @@ async function fetchPlayerDetails(query) {
3214
3365
  var init_player_details = __esm({
3215
3366
  "src/api/player-details.ts"() {
3216
3367
  "use strict";
3368
+ init_date_utils();
3217
3369
  init_errors();
3218
3370
  init_result();
3219
3371
  init_team_mapping();
@@ -3229,80 +3381,82 @@ function toNullable(value) {
3229
3381
  }
3230
3382
  function transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap) {
3231
3383
  const inner = item.player.player.player;
3232
- const stats = item.playerStats.stats;
3233
- const clearances = stats.clearances;
3384
+ const stats = item.playerStats?.stats;
3385
+ const clearances = stats?.clearances;
3234
3386
  return {
3235
3387
  matchId,
3236
3388
  season,
3237
3389
  roundNumber,
3238
- team: normaliseTeamName(teamIdMap?.get(item.teamId) ?? item.teamId),
3390
+ team: normaliseTeamName(
3391
+ teamIdMap?.get(item.teamId) ?? AFL_API_TEAM_IDS.get(item.teamId) ?? item.teamId
3392
+ ),
3239
3393
  competition,
3240
3394
  playerId: inner.playerId,
3241
3395
  givenName: inner.playerName.givenName,
3242
3396
  surname: inner.playerName.surname,
3243
3397
  displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
3244
3398
  jumperNumber: item.player.jumperNumber ?? null,
3245
- kicks: toNullable(stats.kicks),
3246
- handballs: toNullable(stats.handballs),
3247
- disposals: toNullable(stats.disposals),
3248
- marks: toNullable(stats.marks),
3249
- goals: toNullable(stats.goals),
3250
- behinds: toNullable(stats.behinds),
3251
- tackles: toNullable(stats.tackles),
3252
- hitouts: toNullable(stats.hitouts),
3253
- freesFor: toNullable(stats.freesFor),
3254
- freesAgainst: toNullable(stats.freesAgainst),
3255
- contestedPossessions: toNullable(stats.contestedPossessions),
3256
- uncontestedPossessions: toNullable(stats.uncontestedPossessions),
3257
- contestedMarks: toNullable(stats.contestedMarks),
3258
- intercepts: toNullable(stats.intercepts),
3399
+ kicks: toNullable(stats?.kicks),
3400
+ handballs: toNullable(stats?.handballs),
3401
+ disposals: toNullable(stats?.disposals),
3402
+ marks: toNullable(stats?.marks),
3403
+ goals: toNullable(stats?.goals),
3404
+ behinds: toNullable(stats?.behinds),
3405
+ tackles: toNullable(stats?.tackles),
3406
+ hitouts: toNullable(stats?.hitouts),
3407
+ freesFor: toNullable(stats?.freesFor),
3408
+ freesAgainst: toNullable(stats?.freesAgainst),
3409
+ contestedPossessions: toNullable(stats?.contestedPossessions),
3410
+ uncontestedPossessions: toNullable(stats?.uncontestedPossessions),
3411
+ contestedMarks: toNullable(stats?.contestedMarks),
3412
+ intercepts: toNullable(stats?.intercepts),
3259
3413
  centreClearances: toNullable(clearances?.centreClearances),
3260
3414
  stoppageClearances: toNullable(clearances?.stoppageClearances),
3261
3415
  totalClearances: toNullable(clearances?.totalClearances),
3262
- inside50s: toNullable(stats.inside50s),
3263
- rebound50s: toNullable(stats.rebound50s),
3264
- clangers: toNullable(stats.clangers),
3265
- turnovers: toNullable(stats.turnovers),
3266
- onePercenters: toNullable(stats.onePercenters),
3267
- bounces: toNullable(stats.bounces),
3268
- goalAssists: toNullable(stats.goalAssists),
3269
- disposalEfficiency: toNullable(stats.disposalEfficiency),
3270
- metresGained: toNullable(stats.metresGained),
3271
- goalAccuracy: toNullable(stats.goalAccuracy),
3272
- marksInside50: toNullable(stats.marksInside50),
3273
- tacklesInside50: toNullable(stats.tacklesInside50),
3274
- shotsAtGoal: toNullable(stats.shotsAtGoal),
3275
- scoreInvolvements: toNullable(stats.scoreInvolvements),
3276
- totalPossessions: toNullable(stats.totalPossessions),
3277
- timeOnGroundPercentage: toNullable(item.playerStats.timeOnGroundPercentage),
3278
- ratingPoints: toNullable(stats.ratingPoints),
3279
- dreamTeamPoints: toNullable(stats.dreamTeamPoints),
3280
- effectiveDisposals: toNullable(stats.extendedStats?.effectiveDisposals),
3281
- effectiveKicks: toNullable(stats.extendedStats?.effectiveKicks),
3282
- kickEfficiency: toNullable(stats.extendedStats?.kickEfficiency),
3283
- kickToHandballRatio: toNullable(stats.extendedStats?.kickToHandballRatio),
3284
- pressureActs: toNullable(stats.extendedStats?.pressureActs),
3285
- defHalfPressureActs: toNullable(stats.extendedStats?.defHalfPressureActs),
3286
- spoils: toNullable(stats.extendedStats?.spoils),
3287
- hitoutsToAdvantage: toNullable(stats.extendedStats?.hitoutsToAdvantage),
3288
- hitoutWinPercentage: toNullable(stats.extendedStats?.hitoutWinPercentage),
3289
- hitoutToAdvantageRate: toNullable(stats.extendedStats?.hitoutToAdvantageRate),
3290
- groundBallGets: toNullable(stats.extendedStats?.groundBallGets),
3291
- f50GroundBallGets: toNullable(stats.extendedStats?.f50GroundBallGets),
3292
- interceptMarks: toNullable(stats.extendedStats?.interceptMarks),
3293
- marksOnLead: toNullable(stats.extendedStats?.marksOnLead),
3294
- contestedPossessionRate: toNullable(stats.extendedStats?.contestedPossessionRate),
3295
- contestOffOneOnOnes: toNullable(stats.extendedStats?.contestOffOneOnOnes),
3296
- contestOffWins: toNullable(stats.extendedStats?.contestOffWins),
3297
- contestOffWinsPercentage: toNullable(stats.extendedStats?.contestOffWinsPercentage),
3298
- contestDefOneOnOnes: toNullable(stats.extendedStats?.contestDefOneOnOnes),
3299
- contestDefLosses: toNullable(stats.extendedStats?.contestDefLosses),
3300
- contestDefLossPercentage: toNullable(stats.extendedStats?.contestDefLossPercentage),
3301
- centreBounceAttendances: toNullable(stats.extendedStats?.centreBounceAttendances),
3302
- kickins: toNullable(stats.extendedStats?.kickins),
3303
- kickinsPlayon: toNullable(stats.extendedStats?.kickinsPlayon),
3304
- ruckContests: toNullable(stats.extendedStats?.ruckContests),
3305
- scoreLaunches: toNullable(stats.extendedStats?.scoreLaunches),
3416
+ inside50s: toNullable(stats?.inside50s),
3417
+ rebound50s: toNullable(stats?.rebound50s),
3418
+ clangers: toNullable(stats?.clangers),
3419
+ turnovers: toNullable(stats?.turnovers),
3420
+ onePercenters: toNullable(stats?.onePercenters),
3421
+ bounces: toNullable(stats?.bounces),
3422
+ goalAssists: toNullable(stats?.goalAssists),
3423
+ disposalEfficiency: toNullable(stats?.disposalEfficiency),
3424
+ metresGained: toNullable(stats?.metresGained),
3425
+ goalAccuracy: toNullable(stats?.goalAccuracy),
3426
+ marksInside50: toNullable(stats?.marksInside50),
3427
+ tacklesInside50: toNullable(stats?.tacklesInside50),
3428
+ shotsAtGoal: toNullable(stats?.shotsAtGoal),
3429
+ scoreInvolvements: toNullable(stats?.scoreInvolvements),
3430
+ totalPossessions: toNullable(stats?.totalPossessions),
3431
+ timeOnGroundPercentage: toNullable(item.playerStats?.timeOnGroundPercentage),
3432
+ ratingPoints: toNullable(stats?.ratingPoints),
3433
+ dreamTeamPoints: toNullable(stats?.dreamTeamPoints),
3434
+ effectiveDisposals: toNullable(stats?.extendedStats?.effectiveDisposals),
3435
+ effectiveKicks: toNullable(stats?.extendedStats?.effectiveKicks),
3436
+ kickEfficiency: toNullable(stats?.extendedStats?.kickEfficiency),
3437
+ kickToHandballRatio: toNullable(stats?.extendedStats?.kickToHandballRatio),
3438
+ pressureActs: toNullable(stats?.extendedStats?.pressureActs),
3439
+ defHalfPressureActs: toNullable(stats?.extendedStats?.defHalfPressureActs),
3440
+ spoils: toNullable(stats?.extendedStats?.spoils),
3441
+ hitoutsToAdvantage: toNullable(stats?.extendedStats?.hitoutsToAdvantage),
3442
+ hitoutWinPercentage: toNullable(stats?.extendedStats?.hitoutWinPercentage),
3443
+ hitoutToAdvantageRate: toNullable(stats?.extendedStats?.hitoutToAdvantageRate),
3444
+ groundBallGets: toNullable(stats?.extendedStats?.groundBallGets),
3445
+ f50GroundBallGets: toNullable(stats?.extendedStats?.f50GroundBallGets),
3446
+ interceptMarks: toNullable(stats?.extendedStats?.interceptMarks),
3447
+ marksOnLead: toNullable(stats?.extendedStats?.marksOnLead),
3448
+ contestedPossessionRate: toNullable(stats?.extendedStats?.contestedPossessionRate),
3449
+ contestOffOneOnOnes: toNullable(stats?.extendedStats?.contestOffOneOnOnes),
3450
+ contestOffWins: toNullable(stats?.extendedStats?.contestOffWins),
3451
+ contestOffWinsPercentage: toNullable(stats?.extendedStats?.contestOffWinsPercentage),
3452
+ contestDefOneOnOnes: toNullable(stats?.extendedStats?.contestDefOneOnOnes),
3453
+ contestDefLosses: toNullable(stats?.extendedStats?.contestDefLosses),
3454
+ contestDefLossPercentage: toNullable(stats?.extendedStats?.contestDefLossPercentage),
3455
+ centreBounceAttendances: toNullable(stats?.extendedStats?.centreBounceAttendances),
3456
+ kickins: toNullable(stats?.extendedStats?.kickins),
3457
+ kickinsPlayon: toNullable(stats?.extendedStats?.kickinsPlayon),
3458
+ ruckContests: toNullable(stats?.extendedStats?.ruckContests),
3459
+ scoreLaunches: toNullable(stats?.extendedStats?.scoreLaunches),
3306
3460
  source
3307
3461
  };
3308
3462
  }
@@ -3365,8 +3519,9 @@ async function fetchPlayerStats(query) {
3365
3519
  teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
3366
3520
  teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
3367
3521
  }
3368
- const statsResults = await Promise.all(
3369
- matchItemsResult.data.map((item) => client.fetchPlayerStats(item.match.matchId))
3522
+ const statsResults = await batchedMap(
3523
+ matchItemsResult.data,
3524
+ (item) => client.fetchPlayerStats(item.match.matchId)
3370
3525
  );
3371
3526
  const allStats = [];
3372
3527
  for (let i = 0; i < statsResults.length; i++) {
@@ -3390,6 +3545,7 @@ async function fetchPlayerStats(query) {
3390
3545
  return ok(allStats);
3391
3546
  }
3392
3547
  case "footywire": {
3548
+ if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
3393
3549
  const fwClient = new FootyWireClient();
3394
3550
  const idsResult = await fwClient.fetchSeasonMatchIds(query.season);
3395
3551
  if (!idsResult.success) return idsResult;
@@ -3419,6 +3575,7 @@ async function fetchPlayerStats(query) {
3419
3575
  return ok(allStats);
3420
3576
  }
3421
3577
  case "afl-tables": {
3578
+ if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
3422
3579
  const atClient = new AflTablesClient();
3423
3580
  const atResult = await atClient.fetchSeasonPlayerStats(query.season);
3424
3581
  if (!atResult.success) return atResult;
@@ -3434,6 +3591,7 @@ async function fetchPlayerStats(query) {
3434
3591
  var init_player_stats2 = __esm({
3435
3592
  "src/api/player-stats.ts"() {
3436
3593
  "use strict";
3594
+ init_concurrency();
3437
3595
  init_errors();
3438
3596
  init_result();
3439
3597
  init_afl_api();
@@ -3453,7 +3611,37 @@ async function fetchTeamStats(query) {
3453
3611
  }
3454
3612
  case "afl-tables": {
3455
3613
  const client = new AflTablesClient();
3456
- return client.fetchTeamStats(query.season);
3614
+ const statsResult = await client.fetchTeamStats(query.season);
3615
+ if (!statsResult.success) return statsResult;
3616
+ const needsGp = statsResult.data.some((e) => e.gamesPlayed === 0);
3617
+ const gpMap = /* @__PURE__ */ new Map();
3618
+ if (needsGp) {
3619
+ const resultsResult = await client.fetchSeasonResults(query.season);
3620
+ if (resultsResult.success) {
3621
+ for (const m of resultsResult.data) {
3622
+ gpMap.set(m.homeTeam, (gpMap.get(m.homeTeam) ?? 0) + 1);
3623
+ gpMap.set(m.awayTeam, (gpMap.get(m.awayTeam) ?? 0) + 1);
3624
+ }
3625
+ }
3626
+ }
3627
+ const enriched = statsResult.data.map((entry) => ({
3628
+ ...entry,
3629
+ gamesPlayed: gpMap.get(entry.team) ?? entry.gamesPlayed
3630
+ }));
3631
+ if (summaryType === "averages") {
3632
+ return ok(
3633
+ enriched.map((entry) => ({
3634
+ ...entry,
3635
+ stats: Object.fromEntries(
3636
+ Object.entries(entry.stats).map(([k, v]) => [
3637
+ k,
3638
+ entry.gamesPlayed > 0 ? +(v / entry.gamesPlayed).toFixed(1) : 0
3639
+ ])
3640
+ )
3641
+ }))
3642
+ );
3643
+ }
3644
+ return ok(enriched);
3457
3645
  }
3458
3646
  case "afl-api":
3459
3647
  case "squiggle":
@@ -3481,19 +3669,30 @@ var init_team_stats = __esm({
3481
3669
  function teamTypeForComp(comp) {
3482
3670
  return comp === "AFLW" ? "WOMEN" : "MEN";
3483
3671
  }
3484
- async function fetchTeams(query) {
3485
- const client = new AflApiClient();
3486
- const teamType = query?.teamType ?? teamTypeForComp(query?.competition ?? "AFLM");
3487
- const result = await client.fetchTeams(teamType);
3488
- if (!result.success) return result;
3489
- const competition = query?.competition ?? "AFLM";
3490
- const teams = result.data.map((t) => ({
3672
+ function toTeams(data, competition) {
3673
+ return data.map((t) => ({
3491
3674
  teamId: String(t.id),
3492
3675
  name: normaliseTeamName(t.name),
3493
3676
  abbreviation: t.abbreviation ?? "",
3494
3677
  competition
3495
3678
  })).filter((t) => AFL_SENIOR_TEAMS.has(t.name));
3496
- return ok(teams);
3679
+ }
3680
+ async function fetchTeams(query) {
3681
+ const client = new AflApiClient();
3682
+ if (!query?.competition && !query?.teamType) {
3683
+ const [menResult, womenResult] = await Promise.all([
3684
+ client.fetchTeams("MEN"),
3685
+ client.fetchTeams("WOMEN")
3686
+ ]);
3687
+ if (!menResult.success) return menResult;
3688
+ if (!womenResult.success) return womenResult;
3689
+ return ok([...toTeams(menResult.data, "AFLM"), ...toTeams(womenResult.data, "AFLW")]);
3690
+ }
3691
+ const competition = query?.competition ?? "AFLM";
3692
+ const teamType = query?.teamType ?? teamTypeForComp(competition);
3693
+ const result = await client.fetchTeams(teamType);
3694
+ if (!result.success) return result;
3695
+ return ok(toTeams(result.data, competition));
3497
3696
  }
3498
3697
  async function fetchSquad(query) {
3499
3698
  const client = new AflApiClient();
@@ -3514,8 +3713,8 @@ async function fetchSquad(query) {
3514
3713
  jumperNumber: p.jumperNumber ?? null,
3515
3714
  position: p.position ?? null,
3516
3715
  dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
3517
- heightCm: p.player.heightInCm ?? null,
3518
- weightKg: p.player.weightInKg ?? null,
3716
+ heightCm: p.player.heightInCm || null,
3717
+ weightKg: p.player.weightInKg || null,
3519
3718
  draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
3520
3719
  draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
3521
3720
  draftType: p.player.draftType ?? null,
@@ -3556,6 +3755,94 @@ var init_index = __esm({
3556
3755
  }
3557
3756
  });
3558
3757
 
3758
+ // src/cli/flags.ts
3759
+ 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;
3760
+ var init_flags = __esm({
3761
+ "src/cli/flags.ts"() {
3762
+ "use strict";
3763
+ SEASON_FLAG = {
3764
+ season: {
3765
+ type: "string",
3766
+ description: "Season year (e.g. 2025)",
3767
+ required: true,
3768
+ alias: "s"
3769
+ }
3770
+ };
3771
+ OPTIONAL_SEASON_FLAG = {
3772
+ season: {
3773
+ type: "string",
3774
+ description: "Season year (e.g. 2025)",
3775
+ alias: "s"
3776
+ }
3777
+ };
3778
+ ROUND_FLAG = {
3779
+ round: {
3780
+ type: "string",
3781
+ description: "Round number",
3782
+ alias: "r"
3783
+ }
3784
+ };
3785
+ REQUIRED_ROUND_FLAG = {
3786
+ round: {
3787
+ type: "string",
3788
+ description: "Round number",
3789
+ required: true,
3790
+ alias: "r"
3791
+ }
3792
+ };
3793
+ SOURCE_FLAG = {
3794
+ source: {
3795
+ type: "string",
3796
+ description: "Data source",
3797
+ default: "afl-api"
3798
+ }
3799
+ };
3800
+ COMPETITION_FLAG = {
3801
+ competition: {
3802
+ type: "string",
3803
+ description: "Competition code (AFLM or AFLW)",
3804
+ default: "AFLM",
3805
+ alias: "c"
3806
+ }
3807
+ };
3808
+ OPTIONAL_COMPETITION_FLAG = {
3809
+ competition: {
3810
+ type: "string",
3811
+ description: "Competition code (AFLM or AFLW)",
3812
+ alias: "c"
3813
+ }
3814
+ };
3815
+ OUTPUT_FLAGS = {
3816
+ json: { type: "boolean", description: "Output as JSON", alias: "j" },
3817
+ csv: { type: "boolean", description: "Output as CSV" },
3818
+ format: { type: "string", description: "Output format: table, json, csv" },
3819
+ full: { type: "boolean", description: "Show all columns in table output" }
3820
+ };
3821
+ REQUIRED_TEAM_FLAG = {
3822
+ team: {
3823
+ type: "string",
3824
+ description: "Team name, abbreviation, or ID (e.g. Carlton, CARL, 5)",
3825
+ required: true,
3826
+ alias: "t"
3827
+ }
3828
+ };
3829
+ TEAM_FLAG = {
3830
+ team: {
3831
+ type: "string",
3832
+ description: "Filter by team name",
3833
+ alias: "t"
3834
+ }
3835
+ };
3836
+ PLAYER_FLAG = {
3837
+ player: {
3838
+ type: "string",
3839
+ description: "Filter by player name",
3840
+ alias: "p"
3841
+ }
3842
+ };
3843
+ }
3844
+ });
3845
+
3559
3846
  // src/cli/formatters/csv.ts
3560
3847
  function escapeField(value) {
3561
3848
  if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
@@ -3768,10 +4055,6 @@ function validateOptionalSeason(raw) {
3768
4055
  if (raw != null) return validateSeason(raw);
3769
4056
  return void 0;
3770
4057
  }
3771
- function resolveDefaultSeason(competition = "AFLM") {
3772
- const year = (/* @__PURE__ */ new Date()).getFullYear();
3773
- return competition === "AFLW" ? year - 1 : year;
3774
- }
3775
4058
  function validateRound(raw) {
3776
4059
  const round = Number(raw);
3777
4060
  if (Number.isNaN(round) || !Number.isInteger(round) || round < 0) {
@@ -3822,11 +4105,35 @@ function resolveTeamIdentifier(raw, teams) {
3822
4105
  const validNames = teams.map((t) => `${t.name} (${t.abbreviation})`).join(", ");
3823
4106
  throw new Error(`Unknown team: "${raw}" \u2014 valid teams are: ${validNames}`);
3824
4107
  }
4108
+ function resolveMatchByTeam(teamSearch, matchItems) {
4109
+ const normalised = normaliseTeamName(teamSearch);
4110
+ const lower = teamSearch.toLowerCase();
4111
+ const matches = matchItems.filter((item) => {
4112
+ const home = item.match.homeTeam.name;
4113
+ const away = item.match.awayTeam.name;
4114
+ return normaliseTeamName(home) === normalised || normaliseTeamName(away) === normalised || home.toLowerCase().includes(lower) || away.toLowerCase().includes(lower);
4115
+ });
4116
+ const singleMatch = matches[0];
4117
+ if (matches.length === 1 && singleMatch) {
4118
+ return singleMatch.match.matchId;
4119
+ }
4120
+ if (matches.length === 0) {
4121
+ const available = matchItems.map((item) => `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`).join(", ");
4122
+ throw new Error(
4123
+ `No match found for "${teamSearch}" in this round. Available matches: ${available}`
4124
+ );
4125
+ }
4126
+ const ambiguous = matches.map((item) => `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`).join(", ");
4127
+ throw new Error(
4128
+ `Multiple matches found for "${teamSearch}": ${ambiguous}. Please be more specific.`
4129
+ );
4130
+ }
3825
4131
  var VALID_SOURCES, VALID_COMPETITIONS, VALID_FORMATS;
3826
4132
  var init_validation2 = __esm({
3827
4133
  "src/cli/validation.ts"() {
3828
4134
  "use strict";
3829
4135
  init_team_mapping();
4136
+ init_date_utils();
3830
4137
  VALID_SOURCES = ["afl-api", "footywire", "afl-tables", "squiggle"];
3831
4138
  VALID_COMPETITIONS = ["AFLM", "AFLW"];
3832
4139
  VALID_FORMATS = ["table", "json", "csv"];
@@ -3844,6 +4151,8 @@ var init_matches = __esm({
3844
4151
  "src/cli/commands/matches.ts"() {
3845
4152
  "use strict";
3846
4153
  init_index();
4154
+ init_error_boundary();
4155
+ init_flags();
3847
4156
  init_formatters();
3848
4157
  init_ui();
3849
4158
  init_validation2();
@@ -3862,20 +4171,13 @@ var init_matches = __esm({
3862
4171
  description: "Fetch match results for a season"
3863
4172
  },
3864
4173
  args: {
3865
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
3866
- round: { type: "string", description: "Round number" },
3867
- source: { type: "string", description: "Data source", default: "afl-api" },
3868
- competition: {
3869
- type: "string",
3870
- description: "Competition code (AFLM or AFLW)",
3871
- default: "AFLM"
3872
- },
3873
- json: { type: "boolean", description: "Output as JSON" },
3874
- csv: { type: "boolean", description: "Output as CSV" },
3875
- format: { type: "string", description: "Output format: table, json, csv" },
3876
- full: { type: "boolean", description: "Show all columns in table output" }
4174
+ ...SEASON_FLAG,
4175
+ ...ROUND_FLAG,
4176
+ ...SOURCE_FLAG,
4177
+ ...COMPETITION_FLAG,
4178
+ ...OUTPUT_FLAGS
3877
4179
  },
3878
- async run({ args }) {
4180
+ run: withErrorBoundary(async ({ args }) => {
3879
4181
  const season = validateSeason(args.season);
3880
4182
  const round = args.round ? validateRound(args.round) : void 0;
3881
4183
  const source = validateSource(args.source);
@@ -3898,8 +4200,205 @@ var init_matches = __esm({
3898
4200
  columns: DEFAULT_COLUMNS
3899
4201
  };
3900
4202
  console.log(formatOutput(data, formatOptions));
3901
- }
4203
+ })
4204
+ });
4205
+ }
4206
+ });
4207
+
4208
+ // src/lib/fuzzy.ts
4209
+ function levenshteinDistance(a, b) {
4210
+ const la = a.length;
4211
+ const lb = b.length;
4212
+ if (la === 0) return lb;
4213
+ if (lb === 0) return la;
4214
+ if (la < lb) return levenshteinDistance(b, a);
4215
+ const row = Array.from({ length: lb + 1 }, (_, i) => i);
4216
+ for (let i = 1; i <= la; i++) {
4217
+ let prev = i;
4218
+ for (let j = 1; j <= lb; j++) {
4219
+ const current = row[j - 1];
4220
+ const rowJ = row[j];
4221
+ if (current === void 0 || rowJ === void 0) continue;
4222
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4223
+ const val = Math.min(rowJ + 1, prev + 1, current + cost);
4224
+ row[j - 1] = prev;
4225
+ prev = val;
4226
+ }
4227
+ row[lb] = prev;
4228
+ }
4229
+ return row[lb] ?? 0;
4230
+ }
4231
+ function fuzzySearch(query, candidates, keySelector, options) {
4232
+ const maxResults = options?.maxResults ?? 10;
4233
+ const threshold = options?.threshold ?? 0.4;
4234
+ const lowerQuery = query.toLowerCase();
4235
+ const results = [];
4236
+ for (const item of candidates) {
4237
+ const key = keySelector(item).toLowerCase();
4238
+ if (key === lowerQuery) {
4239
+ results.push({ item, score: 0 });
4240
+ continue;
4241
+ }
4242
+ if (key.startsWith(lowerQuery)) {
4243
+ results.push({ item, score: 0.1 });
4244
+ continue;
4245
+ }
4246
+ if (key.includes(lowerQuery)) {
4247
+ results.push({ item, score: 0.3 });
4248
+ continue;
4249
+ }
4250
+ const maxLen = Math.max(lowerQuery.length, key.length);
4251
+ if (maxLen === 0) continue;
4252
+ const distance = levenshteinDistance(lowerQuery, key);
4253
+ const normalised = distance / maxLen;
4254
+ if (normalised <= threshold) {
4255
+ const score = 0.4 + normalised / threshold * 0.6;
4256
+ results.push({ item, score });
4257
+ }
4258
+ }
4259
+ results.sort((a, b) => {
4260
+ if (a.score !== b.score) return a.score - b.score;
4261
+ return keySelector(a.item).localeCompare(keySelector(b.item));
4262
+ });
4263
+ return results.slice(0, maxResults);
4264
+ }
4265
+ var init_fuzzy = __esm({
4266
+ "src/lib/fuzzy.ts"() {
4267
+ "use strict";
4268
+ }
4269
+ });
4270
+
4271
+ // src/cli/resolvers.ts
4272
+ import { isCancel, select } from "@clack/prompts";
4273
+ async function resolveTeamOrPrompt(query, teams) {
4274
+ try {
4275
+ return resolveTeamIdentifier(query, teams);
4276
+ } catch {
4277
+ }
4278
+ const matches = fuzzySearch(query.trim(), teams, (t) => t.name, {
4279
+ maxResults: 5,
4280
+ threshold: 0.4
4281
+ });
4282
+ const abbrevMatches = fuzzySearch(query.trim(), teams, (t) => t.abbreviation, {
4283
+ maxResults: 5,
4284
+ threshold: 0.4
4285
+ });
4286
+ const seen = new Set(matches.map((m) => m.item.teamId));
4287
+ for (const m of abbrevMatches) {
4288
+ if (!seen.has(m.item.teamId)) {
4289
+ matches.push(m);
4290
+ seen.add(m.item.teamId);
4291
+ }
4292
+ }
4293
+ matches.sort((a, b) => a.score - b.score);
4294
+ return disambiguate(
4295
+ query.trim(),
4296
+ matches.map((m) => ({ value: m.item.teamId, label: m.item.name, score: m.score })),
4297
+ teams.map((t) => `${t.name} (${t.abbreviation})`),
4298
+ "team"
4299
+ );
4300
+ }
4301
+ async function resolveTeamNameOrPrompt(query, teamNames) {
4302
+ const trimmed = query.trim();
4303
+ const canonical = normaliseTeamName(trimmed);
4304
+ const candidates = teamNames ?? [...AFL_SENIOR_TEAMS];
4305
+ if (candidates.includes(canonical)) {
4306
+ return canonical;
4307
+ }
4308
+ const items = candidates.map((name) => ({ name }));
4309
+ const matches = fuzzySearch(trimmed, items, (t) => t.name, {
4310
+ maxResults: 5,
4311
+ threshold: 0.4
4312
+ });
4313
+ return disambiguate(
4314
+ trimmed,
4315
+ matches.map((m) => ({ value: m.item.name, label: m.item.name, score: m.score })),
4316
+ candidates,
4317
+ "team"
4318
+ );
4319
+ }
4320
+ async function resolveMatchOrPrompt(query, matchItems) {
4321
+ try {
4322
+ return resolveMatchByTeam(query, matchItems);
4323
+ } catch {
4324
+ }
4325
+ const labelledItems = matchItems.map((item) => ({
4326
+ item,
4327
+ label: `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`
4328
+ }));
4329
+ const matches = fuzzySearch(query, labelledItems, (l) => l.label, {
4330
+ maxResults: 5,
4331
+ threshold: 0.5
4332
+ });
4333
+ const homeMatches = fuzzySearch(query, matchItems, (i) => i.match.homeTeam.name, {
4334
+ maxResults: 5,
4335
+ threshold: 0.4
4336
+ });
4337
+ const awayMatches = fuzzySearch(query, matchItems, (i) => i.match.awayTeam.name, {
4338
+ maxResults: 5,
4339
+ threshold: 0.4
4340
+ });
4341
+ const seen = new Set(matches.map((m) => m.item.item.match.matchId));
4342
+ for (const m of homeMatches) {
4343
+ if (!seen.has(m.item.match.matchId)) {
4344
+ const label = `${m.item.match.homeTeam.name} vs ${m.item.match.awayTeam.name}`;
4345
+ matches.push({ item: { item: m.item, label }, score: m.score });
4346
+ seen.add(m.item.match.matchId);
4347
+ }
4348
+ }
4349
+ for (const m of awayMatches) {
4350
+ if (!seen.has(m.item.match.matchId)) {
4351
+ const label = `${m.item.match.homeTeam.name} vs ${m.item.match.awayTeam.name}`;
4352
+ matches.push({ item: { item: m.item, label }, score: m.score });
4353
+ seen.add(m.item.match.matchId);
4354
+ }
4355
+ }
4356
+ matches.sort((a, b) => a.score - b.score);
4357
+ const available = matchItems.map(
4358
+ (item) => `${item.match.homeTeam.name} vs ${item.match.awayTeam.name}`
4359
+ );
4360
+ return disambiguate(
4361
+ query,
4362
+ matches.map((m) => ({
4363
+ value: m.item.item.match.matchId,
4364
+ label: m.item.label,
4365
+ score: m.score
4366
+ })),
4367
+ available,
4368
+ "match"
4369
+ );
4370
+ }
4371
+ async function disambiguate(query, options, allLabels, entityName) {
4372
+ const best = options[0];
4373
+ if (!best) {
4374
+ throw new Error(
4375
+ `No ${entityName} found for "${query}". Valid options: ${allLabels.join(", ")}`
4376
+ );
4377
+ }
4378
+ if (best.score < 0.2 || options.length === 1) {
4379
+ return best.value;
4380
+ }
4381
+ if (isTTY2) {
4382
+ const choice = await select({
4383
+ message: `Multiple ${entityName}s matched "${query}". Which did you mean?`,
4384
+ options: options.map((o) => ({ value: o.value, label: o.label }))
3902
4385
  });
4386
+ if (isCancel(choice)) {
4387
+ process.exit(0);
4388
+ }
4389
+ return choice;
4390
+ }
4391
+ console.error(`Matched "${query}" \u2192 ${best.label}`);
4392
+ return best.value;
4393
+ }
4394
+ var isTTY2;
4395
+ var init_resolvers = __esm({
4396
+ "src/cli/resolvers.ts"() {
4397
+ "use strict";
4398
+ init_fuzzy();
4399
+ init_team_mapping();
4400
+ init_validation2();
4401
+ isTTY2 = process.stdout.isTTY === true;
3903
4402
  }
3904
4403
  });
3905
4404
 
@@ -3914,7 +4413,12 @@ var init_stats = __esm({
3914
4413
  "src/cli/commands/stats.ts"() {
3915
4414
  "use strict";
3916
4415
  init_index();
4416
+ init_fuzzy();
4417
+ init_afl_api();
4418
+ init_error_boundary();
4419
+ init_flags();
3917
4420
  init_formatters();
4421
+ init_resolvers();
3918
4422
  init_ui();
3919
4423
  init_validation2();
3920
4424
  DEFAULT_COLUMNS2 = [
@@ -3932,27 +4436,32 @@ var init_stats = __esm({
3932
4436
  description: "Fetch player statistics for a season"
3933
4437
  },
3934
4438
  args: {
3935
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
3936
- round: { type: "string", description: "Round number" },
3937
- "match-id": { type: "string", description: "Specific match ID" },
3938
- source: { type: "string", description: "Data source", default: "afl-api" },
3939
- competition: {
3940
- type: "string",
3941
- description: "Competition code (AFLM or AFLW)",
3942
- default: "AFLM"
3943
- },
3944
- json: { type: "boolean", description: "Output as JSON" },
3945
- csv: { type: "boolean", description: "Output as CSV" },
3946
- format: { type: "string", description: "Output format: table, json, csv" },
3947
- full: { type: "boolean", description: "Show all columns in table output" }
4439
+ ...SEASON_FLAG,
4440
+ ...ROUND_FLAG,
4441
+ match: { type: "string", description: "Filter by team name to find a specific match" },
4442
+ "match-id": { type: "string", description: "Specific match provider ID (advanced)" },
4443
+ ...SOURCE_FLAG,
4444
+ ...COMPETITION_FLAG,
4445
+ ...PLAYER_FLAG,
4446
+ ...OUTPUT_FLAGS
3948
4447
  },
3949
- async run({ args }) {
4448
+ run: withErrorBoundary(async ({ args }) => {
3950
4449
  const season = validateSeason(args.season);
3951
4450
  const round = args.round ? validateRound(args.round) : void 0;
3952
- const matchId = args["match-id"];
3953
4451
  const source = validateSource(args.source);
3954
4452
  const competition = validateCompetition(args.competition);
3955
4453
  const format = validateFormat(args.format);
4454
+ let matchId = args["match-id"];
4455
+ if (!matchId && args.match && round != null) {
4456
+ const client = new AflApiClient();
4457
+ const seasonResult = await client.resolveCompSeason(competition, season);
4458
+ if (!seasonResult.success) throw seasonResult.error;
4459
+ const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, round);
4460
+ if (!itemsResult.success) throw itemsResult.error;
4461
+ matchId = await resolveMatchOrPrompt(args.match, itemsResult.data);
4462
+ } else if (args.match && round == null) {
4463
+ throw new Error("--match requires --round (-r) to identify which round to search.");
4464
+ }
3956
4465
  const result = await withSpinner(
3957
4466
  "Fetching player stats\u2026",
3958
4467
  () => fetchPlayerStats({ source, season, round, matchId, competition })
@@ -3960,7 +4469,14 @@ var init_stats = __esm({
3960
4469
  if (!result.success) {
3961
4470
  throw result.error;
3962
4471
  }
3963
- const data = result.data;
4472
+ let data = result.data;
4473
+ if (args.player) {
4474
+ const playerMatches = fuzzySearch(args.player, data, (p) => p.displayName, {
4475
+ maxResults: 50,
4476
+ threshold: 0.4
4477
+ });
4478
+ data = playerMatches.map((m) => m.item);
4479
+ }
3964
4480
  showSummary(
3965
4481
  `Loaded ${data.length} player stat lines for ${season}${round ? ` round ${round}` : ""}`
3966
4482
  );
@@ -3972,7 +4488,7 @@ var init_stats = __esm({
3972
4488
  columns: DEFAULT_COLUMNS2
3973
4489
  };
3974
4490
  console.log(formatOutput(data, formatOptions));
3975
- }
4491
+ })
3976
4492
  });
3977
4493
  }
3978
4494
  });
@@ -3988,6 +4504,8 @@ var init_fixture2 = __esm({
3988
4504
  "src/cli/commands/fixture.ts"() {
3989
4505
  "use strict";
3990
4506
  init_index();
4507
+ init_error_boundary();
4508
+ init_flags();
3991
4509
  init_formatters();
3992
4510
  init_ui();
3993
4511
  init_validation2();
@@ -4004,20 +4522,13 @@ var init_fixture2 = __esm({
4004
4522
  description: "Fetch fixture/schedule for a season"
4005
4523
  },
4006
4524
  args: {
4007
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4008
- round: { type: "string", description: "Round number" },
4009
- source: { type: "string", description: "Data source", default: "afl-api" },
4010
- competition: {
4011
- type: "string",
4012
- description: "Competition code (AFLM or AFLW)",
4013
- default: "AFLM"
4014
- },
4015
- json: { type: "boolean", description: "Output as JSON" },
4016
- csv: { type: "boolean", description: "Output as CSV" },
4017
- format: { type: "string", description: "Output format: table, json, csv" },
4018
- full: { type: "boolean", description: "Show all columns in table output" }
4525
+ ...SEASON_FLAG,
4526
+ ...ROUND_FLAG,
4527
+ ...SOURCE_FLAG,
4528
+ ...COMPETITION_FLAG,
4529
+ ...OUTPUT_FLAGS
4019
4530
  },
4020
- async run({ args }) {
4531
+ run: withErrorBoundary(async ({ args }) => {
4021
4532
  const season = validateSeason(args.season);
4022
4533
  const round = args.round ? validateRound(args.round) : void 0;
4023
4534
  const source = validateSource(args.source);
@@ -4040,7 +4551,7 @@ var init_fixture2 = __esm({
4040
4551
  columns: DEFAULT_COLUMNS3
4041
4552
  };
4042
4553
  console.log(formatOutput(data, formatOptions));
4043
- }
4554
+ })
4044
4555
  });
4045
4556
  }
4046
4557
  });
@@ -4056,6 +4567,8 @@ var init_ladder3 = __esm({
4056
4567
  "src/cli/commands/ladder.ts"() {
4057
4568
  "use strict";
4058
4569
  init_index();
4570
+ init_error_boundary();
4571
+ init_flags();
4059
4572
  init_formatters();
4060
4573
  init_ui();
4061
4574
  init_validation2();
@@ -4074,20 +4587,13 @@ var init_ladder3 = __esm({
4074
4587
  description: "Fetch ladder standings for a season"
4075
4588
  },
4076
4589
  args: {
4077
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4078
- round: { type: "string", description: "Round number" },
4079
- source: { type: "string", description: "Data source", default: "afl-api" },
4080
- competition: {
4081
- type: "string",
4082
- description: "Competition code (AFLM or AFLW)",
4083
- default: "AFLM"
4084
- },
4085
- json: { type: "boolean", description: "Output as JSON" },
4086
- csv: { type: "boolean", description: "Output as CSV" },
4087
- format: { type: "string", description: "Output format: table, json, csv" },
4088
- full: { type: "boolean", description: "Show all columns in table output" }
4590
+ ...SEASON_FLAG,
4591
+ ...ROUND_FLAG,
4592
+ ...SOURCE_FLAG,
4593
+ ...COMPETITION_FLAG,
4594
+ ...OUTPUT_FLAGS
4089
4595
  },
4090
- async run({ args }) {
4596
+ run: withErrorBoundary(async ({ args }) => {
4091
4597
  const season = validateSeason(args.season);
4092
4598
  const round = args.round ? validateRound(args.round) : void 0;
4093
4599
  const source = validateSource(args.source);
@@ -4112,7 +4618,7 @@ var init_ladder3 = __esm({
4112
4618
  columns: DEFAULT_COLUMNS4
4113
4619
  };
4114
4620
  console.log(formatOutput(data.entries, formatOptions));
4115
- }
4621
+ })
4116
4622
  });
4117
4623
  }
4118
4624
  });
@@ -4150,7 +4656,11 @@ var init_lineup3 = __esm({
4150
4656
  "src/cli/commands/lineup.ts"() {
4151
4657
  "use strict";
4152
4658
  init_index();
4659
+ init_afl_api();
4660
+ init_error_boundary();
4661
+ init_flags();
4153
4662
  init_formatters();
4663
+ init_resolvers();
4154
4664
  init_ui();
4155
4665
  init_validation2();
4156
4666
  DEFAULT_COLUMNS5 = [
@@ -4166,27 +4676,29 @@ var init_lineup3 = __esm({
4166
4676
  description: "Fetch match lineups for a round"
4167
4677
  },
4168
4678
  args: {
4169
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4170
- round: { type: "string", description: "Round number", required: true },
4171
- "match-id": { type: "string", description: "Specific match ID" },
4172
- source: { type: "string", description: "Data source", default: "afl-api" },
4173
- competition: {
4174
- type: "string",
4175
- description: "Competition code (AFLM or AFLW)",
4176
- default: "AFLM"
4177
- },
4178
- json: { type: "boolean", description: "Output as JSON" },
4179
- csv: { type: "boolean", description: "Output as CSV" },
4180
- format: { type: "string", description: "Output format: table, json, csv" },
4181
- full: { type: "boolean", description: "Show all columns in table output" }
4679
+ ...SEASON_FLAG,
4680
+ ...REQUIRED_ROUND_FLAG,
4681
+ match: { type: "string", description: "Filter by team name to find a specific match" },
4682
+ "match-id": { type: "string", description: "Specific match provider ID (advanced)" },
4683
+ ...SOURCE_FLAG,
4684
+ ...COMPETITION_FLAG,
4685
+ ...OUTPUT_FLAGS
4182
4686
  },
4183
- async run({ args }) {
4687
+ run: withErrorBoundary(async ({ args }) => {
4184
4688
  const season = validateSeason(args.season);
4185
4689
  const round = validateRound(args.round);
4186
- const matchId = args["match-id"];
4187
4690
  const source = validateSource(args.source);
4188
4691
  const competition = validateCompetition(args.competition);
4189
4692
  const format = validateFormat(args.format);
4693
+ let matchId = args["match-id"];
4694
+ if (!matchId && args.match) {
4695
+ const client = new AflApiClient();
4696
+ const seasonResult = await client.resolveCompSeason(competition, season);
4697
+ if (!seasonResult.success) throw seasonResult.error;
4698
+ const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, round);
4699
+ if (!itemsResult.success) throw itemsResult.error;
4700
+ matchId = await resolveMatchOrPrompt(args.match, itemsResult.data);
4701
+ }
4190
4702
  const result = await withSpinner(
4191
4703
  "Fetching lineups\u2026",
4192
4704
  () => fetchLineup({ source, season, round, matchId, competition })
@@ -4209,7 +4721,7 @@ var init_lineup3 = __esm({
4209
4721
  } else {
4210
4722
  console.log(formatOutput(flattenLineups(data), formatOptions));
4211
4723
  }
4212
- }
4724
+ })
4213
4725
  });
4214
4726
  }
4215
4727
  });
@@ -4225,7 +4737,10 @@ var init_squad = __esm({
4225
4737
  "src/cli/commands/squad.ts"() {
4226
4738
  "use strict";
4227
4739
  init_index();
4740
+ init_error_boundary();
4741
+ init_flags();
4228
4742
  init_formatters();
4743
+ init_resolvers();
4229
4744
  init_ui();
4230
4745
  init_validation2();
4231
4746
  DEFAULT_COLUMNS6 = [
@@ -4241,34 +4756,22 @@ var init_squad = __esm({
4241
4756
  description: "Fetch team squad for a season"
4242
4757
  },
4243
4758
  args: {
4244
- "team-id": {
4245
- type: "string",
4246
- description: "Team ID, abbreviation, or name (e.g. 5, CARL, Carlton)",
4247
- required: true
4248
- },
4249
- season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4250
- competition: {
4251
- type: "string",
4252
- description: "Competition code (AFLM or AFLW)",
4253
- default: "AFLM"
4254
- },
4255
- json: { type: "boolean", description: "Output as JSON" },
4256
- csv: { type: "boolean", description: "Output as CSV" },
4257
- format: { type: "string", description: "Output format: table, json, csv" },
4258
- full: { type: "boolean", description: "Show all columns in table output" }
4759
+ ...REQUIRED_TEAM_FLAG,
4760
+ ...SEASON_FLAG,
4761
+ ...COMPETITION_FLAG,
4762
+ ...OUTPUT_FLAGS
4259
4763
  },
4260
- async run({ args }) {
4764
+ run: withErrorBoundary(async ({ args }) => {
4261
4765
  const season = validateSeason(args.season);
4262
4766
  const competition = validateCompetition(args.competition);
4263
4767
  const format = validateFormat(args.format);
4264
- let teamId = args["team-id"].trim();
4265
- const isNumeric = /^\d+$/.test(teamId);
4266
- if (!isNumeric) {
4768
+ let teamId = args.team.trim();
4769
+ if (!/^\d+$/.test(teamId)) {
4267
4770
  const teamsResult = await withSpinner("Resolving team\u2026", () => fetchTeams({ competition }));
4268
4771
  if (!teamsResult.success) {
4269
4772
  throw teamsResult.error;
4270
4773
  }
4271
- teamId = resolveTeamIdentifier(args["team-id"], teamsResult.data);
4774
+ teamId = await resolveTeamOrPrompt(args.team, teamsResult.data);
4272
4775
  }
4273
4776
  const result = await withSpinner(
4274
4777
  "Fetching squad\u2026",
@@ -4287,7 +4790,7 @@ var init_squad = __esm({
4287
4790
  columns: DEFAULT_COLUMNS6
4288
4791
  };
4289
4792
  console.log(formatOutput(data.players, formatOptions));
4290
- }
4793
+ })
4291
4794
  });
4292
4795
  }
4293
4796
  });
@@ -4303,6 +4806,8 @@ var init_teams2 = __esm({
4303
4806
  "src/cli/commands/teams.ts"() {
4304
4807
  "use strict";
4305
4808
  init_index();
4809
+ init_error_boundary();
4810
+ init_flags();
4306
4811
  init_formatters();
4307
4812
  init_ui();
4308
4813
  init_validation2();
@@ -4318,14 +4823,11 @@ var init_teams2 = __esm({
4318
4823
  description: "Fetch team list"
4319
4824
  },
4320
4825
  args: {
4321
- competition: { type: "string", description: "Competition code (AFLM or AFLW)" },
4826
+ ...OPTIONAL_COMPETITION_FLAG,
4322
4827
  "team-type": { type: "string", description: "Team type filter" },
4323
- json: { type: "boolean", description: "Output as JSON" },
4324
- csv: { type: "boolean", description: "Output as CSV" },
4325
- format: { type: "string", description: "Output format: table, json, csv" },
4326
- full: { type: "boolean", description: "Show all columns in table output" }
4828
+ ...OUTPUT_FLAGS
4327
4829
  },
4328
- async run({ args }) {
4830
+ run: withErrorBoundary(async ({ args }) => {
4329
4831
  const competition = validateOptionalCompetition(args.competition);
4330
4832
  const format = validateFormat(args.format);
4331
4833
  const result = await withSpinner(
@@ -4350,7 +4852,7 @@ var init_teams2 = __esm({
4350
4852
  columns: DEFAULT_COLUMNS7
4351
4853
  };
4352
4854
  console.log(formatOutput(data, formatOptions));
4353
- }
4855
+ })
4354
4856
  });
4355
4857
  }
4356
4858
  });
@@ -4364,14 +4866,20 @@ import { defineCommand as defineCommand8 } from "citty";
4364
4866
  function flattenEntries(data) {
4365
4867
  return data.map((entry) => {
4366
4868
  const { stats, ...rest } = entry;
4367
- return { ...rest, ...stats };
4869
+ const normalised = {};
4870
+ for (const [key, value] of Object.entries(stats)) {
4871
+ normalised[AFL_TABLES_KEY_MAP[key] ?? key] = value;
4872
+ }
4873
+ return { ...rest, ...normalised };
4368
4874
  });
4369
4875
  }
4370
- var DEFAULT_COLUMNS8, teamStatsCommand;
4876
+ var DEFAULT_COLUMNS8, AFL_TABLES_KEY_MAP, teamStatsCommand;
4371
4877
  var init_team_stats2 = __esm({
4372
4878
  "src/cli/commands/team-stats.ts"() {
4373
4879
  "use strict";
4374
4880
  init_index();
4881
+ init_error_boundary();
4882
+ init_flags();
4375
4883
  init_formatters();
4376
4884
  init_ui();
4377
4885
  init_validation2();
@@ -4387,25 +4895,69 @@ var init_team_stats2 = __esm({
4387
4895
  { key: "T", label: "T", maxWidth: 6 },
4388
4896
  { key: "I50", label: "I50", maxWidth: 6 }
4389
4897
  ];
4898
+ AFL_TABLES_KEY_MAP = {
4899
+ KI_for: "K",
4900
+ MK_for: "M",
4901
+ HB_for: "HB",
4902
+ DI_for: "D",
4903
+ GL_for: "G",
4904
+ BH_for: "B",
4905
+ HO_for: "HO",
4906
+ TK_for: "T",
4907
+ RB_for: "RB",
4908
+ IF_for: "IF",
4909
+ CL_for: "CL",
4910
+ CG_for: "CG",
4911
+ FF_for: "FF",
4912
+ BR_for: "BR",
4913
+ CP_for: "CP",
4914
+ UP_for: "UP",
4915
+ CM_for: "CM",
4916
+ MI_for: "MI",
4917
+ "1%_for": "1%",
4918
+ BO_for: "BO",
4919
+ GA_for: "GA",
4920
+ I50_for: "I50",
4921
+ // "against" variants
4922
+ KI_against: "K_against",
4923
+ MK_against: "M_against",
4924
+ HB_against: "HB_against",
4925
+ DI_against: "D_against",
4926
+ GL_against: "G_against",
4927
+ BH_against: "B_against",
4928
+ HO_against: "HO_against",
4929
+ TK_against: "T_against",
4930
+ RB_against: "RB_against",
4931
+ IF_against: "IF_against",
4932
+ CL_against: "CL_against",
4933
+ CG_against: "CG_against",
4934
+ FF_against: "FF_against",
4935
+ BR_against: "BR_against",
4936
+ CP_against: "CP_against",
4937
+ UP_against: "UP_against",
4938
+ CM_against: "CM_against",
4939
+ MI_against: "MI_against",
4940
+ "1%_against": "1%_against",
4941
+ BO_against: "BO_against",
4942
+ GA_against: "GA_against",
4943
+ I50_against: "I50_against"
4944
+ };
4390
4945
  teamStatsCommand = defineCommand8({
4391
4946
  meta: {
4392
4947
  name: "team-stats",
4393
4948
  description: "Fetch team aggregate statistics for a season"
4394
4949
  },
4395
4950
  args: {
4396
- season: { type: "string", description: "Season year (e.g. 2024)", required: true },
4951
+ ...SEASON_FLAG,
4397
4952
  source: {
4398
4953
  type: "string",
4399
4954
  description: "Data source (footywire, afl-tables)",
4400
4955
  default: "footywire"
4401
4956
  },
4402
4957
  summary: { type: "string", description: "Summary type: totals or averages", default: "totals" },
4403
- json: { type: "boolean", description: "Output as JSON" },
4404
- csv: { type: "boolean", description: "Output as CSV" },
4405
- format: { type: "string", description: "Output format: table, json, csv" },
4406
- full: { type: "boolean", description: "Show all columns in table output" }
4958
+ ...OUTPUT_FLAGS
4407
4959
  },
4408
- async run({ args }) {
4960
+ run: withErrorBoundary(async ({ args }) => {
4409
4961
  const season = validateSeason(args.season);
4410
4962
  const source = validateSource(args.source);
4411
4963
  const format = validateFormat(args.format);
@@ -4428,7 +4980,7 @@ var init_team_stats2 = __esm({
4428
4980
  columns: DEFAULT_COLUMNS8
4429
4981
  };
4430
4982
  console.log(formatOutput(flat, formatOptions));
4431
- }
4983
+ })
4432
4984
  });
4433
4985
  }
4434
4986
  });
@@ -4444,7 +4996,10 @@ var init_player_details2 = __esm({
4444
4996
  "src/cli/commands/player-details.ts"() {
4445
4997
  "use strict";
4446
4998
  init_index();
4999
+ init_error_boundary();
5000
+ init_flags();
4447
5001
  init_formatters();
5002
+ init_resolvers();
4448
5003
  init_ui();
4449
5004
  init_validation2();
4450
5005
  DEFAULT_COLUMNS9 = [
@@ -4462,37 +5017,31 @@ var init_player_details2 = __esm({
4462
5017
  description: "Fetch player biographical details for a team"
4463
5018
  },
4464
5019
  args: {
4465
- team: { type: "positional", description: "Team name (e.g. Carlton, Hawthorn)", required: true },
5020
+ ...REQUIRED_TEAM_FLAG,
4466
5021
  source: {
4467
5022
  type: "string",
4468
5023
  description: "Data source: afl-api, footywire, afl-tables",
4469
5024
  default: "afl-api"
4470
5025
  },
4471
- season: { type: "string", description: "Season year (for AFL API source, e.g. 2025)" },
4472
- competition: {
4473
- type: "string",
4474
- description: "Competition code (AFLM or AFLW)",
4475
- default: "AFLM"
4476
- },
4477
- json: { type: "boolean", description: "Output as JSON" },
4478
- csv: { type: "boolean", description: "Output as CSV" },
4479
- format: { type: "string", description: "Output format: table, json, csv" },
4480
- full: { type: "boolean", description: "Show all columns in table output" }
5026
+ ...OPTIONAL_SEASON_FLAG,
5027
+ ...COMPETITION_FLAG,
5028
+ ...OUTPUT_FLAGS
4481
5029
  },
4482
- async run({ args }) {
5030
+ run: withErrorBoundary(async ({ args }) => {
4483
5031
  const source = validateSource(args.source);
4484
5032
  const competition = validateCompetition(args.competition);
4485
5033
  const format = validateFormat(args.format);
4486
5034
  const season = validateOptionalSeason(args.season) ?? resolveDefaultSeason(competition);
5035
+ const team = await resolveTeamNameOrPrompt(args.team);
4487
5036
  const result = await withSpinner(
4488
5037
  "Fetching player details\u2026",
4489
- () => fetchPlayerDetails({ source, team: args.team, season, competition })
5038
+ () => fetchPlayerDetails({ source, team, season, competition })
4490
5039
  );
4491
5040
  if (!result.success) {
4492
5041
  throw result.error;
4493
5042
  }
4494
5043
  const data = result.data;
4495
- showSummary(`Loaded ${data.length} players for ${args.team} (${source})`);
5044
+ showSummary(`Loaded ${data.length} players for ${team} (${source})`);
4496
5045
  const formatOptions = {
4497
5046
  json: args.json,
4498
5047
  csv: args.csv,
@@ -4501,7 +5050,7 @@ var init_player_details2 = __esm({
4501
5050
  columns: DEFAULT_COLUMNS9
4502
5051
  };
4503
5052
  console.log(formatOutput(data, formatOptions));
4504
- }
5053
+ })
4505
5054
  });
4506
5055
  }
4507
5056
  });
@@ -4517,7 +5066,10 @@ var init_coaches_votes2 = __esm({
4517
5066
  "src/cli/commands/coaches-votes.ts"() {
4518
5067
  "use strict";
4519
5068
  init_index();
5069
+ init_error_boundary();
5070
+ init_flags();
4520
5071
  init_formatters();
5072
+ init_resolvers();
4521
5073
  init_ui();
4522
5074
  init_validation2();
4523
5075
  DEFAULT_COLUMNS10 = [
@@ -4534,33 +5086,27 @@ var init_coaches_votes2 = __esm({
4534
5086
  description: "Fetch AFLCA coaches votes for a season"
4535
5087
  },
4536
5088
  args: {
4537
- season: { type: "string", description: "Season year (e.g. 2024)", required: true },
4538
- round: { type: "string", description: "Round number" },
4539
- competition: {
4540
- type: "string",
4541
- description: "Competition code (AFLM or AFLW)",
4542
- default: "AFLM"
4543
- },
4544
- team: { type: "string", description: "Filter by team name" },
4545
- json: { type: "boolean", description: "Output as JSON" },
4546
- csv: { type: "boolean", description: "Output as CSV" },
4547
- format: { type: "string", description: "Output format: table, json, csv" },
4548
- full: { type: "boolean", description: "Show all columns in table output" }
5089
+ ...SEASON_FLAG,
5090
+ ...ROUND_FLAG,
5091
+ ...COMPETITION_FLAG,
5092
+ ...TEAM_FLAG,
5093
+ ...OUTPUT_FLAGS
4549
5094
  },
4550
- async run({ args }) {
5095
+ run: withErrorBoundary(async ({ args }) => {
4551
5096
  const season = validateSeason(args.season);
4552
5097
  const round = args.round ? validateRound(args.round) : void 0;
4553
5098
  const competition = validateCompetition(args.competition);
4554
5099
  const format = validateFormat(args.format);
5100
+ const team = args.team ? await resolveTeamNameOrPrompt(args.team) : void 0;
4555
5101
  const result = await withSpinner(
4556
5102
  "Fetching coaches votes\u2026",
4557
- () => fetchCoachesVotes({ season, round, competition, team: args.team })
5103
+ () => fetchCoachesVotes({ season, round, competition, team })
4558
5104
  );
4559
5105
  if (!result.success) {
4560
5106
  throw result.error;
4561
5107
  }
4562
5108
  const data = result.data;
4563
- const teamSuffix = args.team ? ` for ${args.team}` : "";
5109
+ const teamSuffix = team ? ` for ${team}` : "";
4564
5110
  const roundSuffix = round ? ` round ${round}` : "";
4565
5111
  showSummary(`Loaded ${data.length} vote records for ${season}${roundSuffix}${teamSuffix}`);
4566
5112
  const formatOptions = {
@@ -4571,7 +5117,7 @@ var init_coaches_votes2 = __esm({
4571
5117
  columns: DEFAULT_COLUMNS10
4572
5118
  };
4573
5119
  console.log(formatOutput(data, formatOptions));
4574
- }
5120
+ })
4575
5121
  });
4576
5122
  }
4577
5123
  });
@@ -4579,37 +5125,34 @@ var init_coaches_votes2 = __esm({
4579
5125
  // src/cli.ts
4580
5126
  import { defineCommand as defineCommand11, runMain } from "citty";
4581
5127
 
4582
- // src/cli/error-boundary.ts
4583
- init_errors();
4584
- import pc from "picocolors";
4585
- function formatError(error) {
4586
- if (error instanceof ValidationError && error.issues) {
4587
- const issueLines = error.issues.map((i) => ` ${pc.yellow(i.path)}: ${i.message}`);
4588
- return `${pc.red("Validation error:")}
4589
- ${issueLines.join("\n")}`;
4590
- }
4591
- if (error instanceof AflApiError) {
4592
- const status = error.statusCode ? ` (HTTP ${error.statusCode})` : "";
4593
- return `${pc.red("AFL API error:")} ${error.message}${status}`;
4594
- }
4595
- if (error instanceof ScrapeError) {
4596
- const source = error.source ? ` [${error.source}]` : "";
4597
- return `${pc.red("Scrape error:")} ${error.message}${source}`;
4598
- }
4599
- if (error instanceof UnsupportedSourceError) {
4600
- return `${pc.red("Unsupported source:")} ${error.message}`;
4601
- }
4602
- if (error instanceof Error) {
4603
- return `${pc.red("Error:")} ${error.message}`;
5128
+ // src/cli/alias-resolution.ts
5129
+ var SHORT_TO_LONG = {
5130
+ "-s": "--season",
5131
+ "-r": "--round",
5132
+ "-c": "--competition",
5133
+ "-j": "--json",
5134
+ "-t": "--team",
5135
+ "-p": "--player"
5136
+ };
5137
+ function resolveAliases() {
5138
+ for (let i = 0; i < process.argv.length; i++) {
5139
+ const arg = process.argv[i];
5140
+ if (arg != null) {
5141
+ const long = SHORT_TO_LONG[arg];
5142
+ if (long) {
5143
+ process.argv[i] = long;
5144
+ }
5145
+ }
4604
5146
  }
4605
- return `${pc.red("Error:")} ${String(error)}`;
4606
5147
  }
4607
5148
 
4608
5149
  // src/cli.ts
5150
+ init_error_boundary();
5151
+ resolveAliases();
4609
5152
  var main = defineCommand11({
4610
5153
  meta: {
4611
5154
  name: "fitzroy",
4612
- version: "1.1.1",
5155
+ version: "1.3.0",
4613
5156
  description: "CLI for fetching AFL data \u2014 match results, player stats, fixtures, ladders, and more"
4614
5157
  },
4615
5158
  subCommands: {