fitzroy 1.1.0 → 1.1.1

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,
@@ -1292,7 +1303,7 @@ var init_coaches_votes = __esm({
1292
1303
 
1293
1304
  // src/lib/validation.ts
1294
1305
  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;
1306
+ 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
1307
  var init_validation = __esm({
1297
1308
  "src/lib/validation.ts"() {
1298
1309
  "use strict";
@@ -1399,71 +1410,72 @@ var init_validation = __esm({
1399
1410
  captain: z.boolean().optional(),
1400
1411
  playerJumperNumber: z.number().optional()
1401
1412
  }).passthrough();
1413
+ statNum = z.number().nullable().optional();
1402
1414
  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(),
1415
+ goals: statNum,
1416
+ behinds: statNum,
1417
+ kicks: statNum,
1418
+ handballs: statNum,
1419
+ disposals: statNum,
1420
+ marks: statNum,
1421
+ bounces: statNum,
1422
+ tackles: statNum,
1423
+ contestedPossessions: statNum,
1424
+ uncontestedPossessions: statNum,
1425
+ totalPossessions: statNum,
1426
+ inside50s: statNum,
1427
+ marksInside50: statNum,
1428
+ contestedMarks: statNum,
1429
+ hitouts: statNum,
1430
+ onePercenters: statNum,
1431
+ disposalEfficiency: statNum,
1432
+ clangers: statNum,
1433
+ freesFor: statNum,
1434
+ freesAgainst: statNum,
1435
+ dreamTeamPoints: statNum,
1424
1436
  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(),
1437
+ centreClearances: statNum,
1438
+ stoppageClearances: statNum,
1439
+ totalClearances: statNum
1440
+ }).passthrough().nullable().optional(),
1441
+ rebound50s: statNum,
1442
+ goalAssists: statNum,
1443
+ goalAccuracy: statNum,
1444
+ turnovers: statNum,
1445
+ intercepts: statNum,
1446
+ tacklesInside50: statNum,
1447
+ shotsAtGoal: statNum,
1448
+ metresGained: statNum,
1449
+ scoreInvolvements: statNum,
1450
+ ratingPoints: statNum,
1439
1451
  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()
1452
+ effectiveDisposals: statNum,
1453
+ effectiveKicks: statNum,
1454
+ kickEfficiency: statNum,
1455
+ kickToHandballRatio: statNum,
1456
+ pressureActs: statNum,
1457
+ defHalfPressureActs: statNum,
1458
+ spoils: statNum,
1459
+ hitoutsToAdvantage: statNum,
1460
+ hitoutWinPercentage: statNum,
1461
+ hitoutToAdvantageRate: statNum,
1462
+ groundBallGets: statNum,
1463
+ f50GroundBallGets: statNum,
1464
+ interceptMarks: statNum,
1465
+ marksOnLead: statNum,
1466
+ contestedPossessionRate: statNum,
1467
+ contestOffOneOnOnes: statNum,
1468
+ contestOffWins: statNum,
1469
+ contestOffWinsPercentage: statNum,
1470
+ contestDefOneOnOnes: statNum,
1471
+ contestDefLosses: statNum,
1472
+ contestDefLossPercentage: statNum,
1473
+ centreBounceAttendances: statNum,
1474
+ kickins: statNum,
1475
+ kickinsPlayon: statNum,
1476
+ ruckContests: statNum,
1477
+ scoreLaunches: statNum
1478
+ }).passthrough().nullable().optional()
1467
1479
  }).passthrough();
1468
1480
  PlayerStatsItemSchema = z.object({
1469
1481
  player: z.object({
@@ -1476,7 +1488,7 @@ var init_validation = __esm({
1476
1488
  teamId: z.string(),
1477
1489
  playerStats: z.object({
1478
1490
  stats: PlayerGameStatsSchema,
1479
- timeOnGroundPercentage: z.number().optional()
1491
+ timeOnGroundPercentage: z.number().nullable().optional()
1480
1492
  }).passthrough()
1481
1493
  }).passthrough();
1482
1494
  PlayerStatsListSchema = z.object({
@@ -2486,6 +2498,7 @@ function parseAflTablesTeamStats(html, year) {
2486
2498
  $(rows[0]).find("td, th").each((_ci, cell) => {
2487
2499
  headers.push($(cell).text().trim());
2488
2500
  });
2501
+ const gpColIdx = headers.findIndex((h, i) => i > 0 && GP_HEADERS.has(h.toLowerCase()));
2489
2502
  for (let ri = 1; ri < rows.length; ri++) {
2490
2503
  const cells = $(rows[ri]).find("td");
2491
2504
  if (cells.length < 3) continue;
@@ -2498,7 +2511,12 @@ function parseAflTablesTeamStats(html, year) {
2498
2511
  }
2499
2512
  const entry = teamMap.get(teamName);
2500
2513
  if (!entry) continue;
2514
+ if (gpColIdx >= 0 && suffix === "_for") {
2515
+ const gpVal = Number.parseFloat($(cells[gpColIdx]).text().trim().replace(/,/g, "")) || 0;
2516
+ entry.gamesPlayed = gpVal;
2517
+ }
2501
2518
  for (let ci = 1; ci < cells.length; ci++) {
2519
+ if (ci === gpColIdx) continue;
2502
2520
  const header = headers[ci];
2503
2521
  if (!header) continue;
2504
2522
  const value = Number.parseFloat($(cells[ci]).text().trim().replace(/,/g, "")) || 0;
@@ -2574,7 +2592,7 @@ function parseAflTablesPlayerList(html, teamName) {
2574
2592
  });
2575
2593
  return players;
2576
2594
  }
2577
- var AFL_TABLES_BASE, AflTablesClient, AFL_TABLES_SLUG_MAP;
2595
+ var AFL_TABLES_BASE, AflTablesClient, GP_HEADERS, AFL_TABLES_SLUG_MAP;
2578
2596
  var init_afl_tables = __esm({
2579
2597
  "src/sources/afl-tables.ts"() {
2580
2598
  "use strict";
@@ -2756,6 +2774,7 @@ var init_afl_tables = __esm({
2756
2774
  }
2757
2775
  }
2758
2776
  };
2777
+ GP_HEADERS = /* @__PURE__ */ new Set(["gm", "gp", "p", "mp", "games"]);
2759
2778
  AFL_TABLES_SLUG_MAP = /* @__PURE__ */ new Map([
2760
2779
  ["Adelaide Crows", "adelaide"],
2761
2780
  ["Brisbane Lions", "brisbane"],
@@ -3310,15 +3329,26 @@ async function fetchPlayerStats(query) {
3310
3329
  case "afl-api": {
3311
3330
  const client = new AflApiClient();
3312
3331
  if (query.matchId) {
3313
- const result = await client.fetchPlayerStats(query.matchId);
3314
- if (!result.success) return result;
3332
+ const [rosterResult, statsResult] = await Promise.all([
3333
+ client.fetchMatchRoster(query.matchId),
3334
+ client.fetchPlayerStats(query.matchId)
3335
+ ]);
3336
+ if (!statsResult.success) return statsResult;
3337
+ const teamIdMap2 = /* @__PURE__ */ new Map();
3338
+ if (rosterResult.success) {
3339
+ const match = rosterResult.data.match;
3340
+ teamIdMap2.set(match.homeTeamId, match.homeTeam.name);
3341
+ teamIdMap2.set(match.awayTeamId, match.awayTeam.name);
3342
+ }
3315
3343
  return ok(
3316
3344
  transformPlayerStats(
3317
- result.data,
3345
+ statsResult.data,
3318
3346
  query.matchId,
3319
3347
  query.season,
3320
3348
  query.round ?? 0,
3321
- competition
3349
+ competition,
3350
+ "afl-api",
3351
+ teamIdMap2.size > 0 ? teamIdMap2 : void 0
3322
3352
  )
3323
3353
  );
3324
3354
  }
@@ -3570,7 +3600,7 @@ var init_json = __esm({
3570
3600
  // src/cli/formatters/table.ts
3571
3601
  function toDisplayValue(value) {
3572
3602
  if (value === null || value === void 0) return "-";
3573
- if (value instanceof Date) return value.toISOString().slice(0, 16).replace("T", " ");
3603
+ if (value instanceof Date) return AEST_COMPACT_FORMATTER.format(value);
3574
3604
  if (typeof value === "object") return JSON.stringify(value);
3575
3605
  return String(value);
3576
3606
  }
@@ -3644,9 +3674,18 @@ function formatTable(data, options = {}) {
3644
3674
  });
3645
3675
  return [header, separator, ...rows].join("\n");
3646
3676
  }
3677
+ var AEST_COMPACT_FORMATTER;
3647
3678
  var init_table = __esm({
3648
3679
  "src/cli/formatters/table.ts"() {
3649
3680
  "use strict";
3681
+ AEST_COMPACT_FORMATTER = new Intl.DateTimeFormat("en-AU", {
3682
+ timeZone: "Australia/Melbourne",
3683
+ day: "numeric",
3684
+ month: "short",
3685
+ hour: "numeric",
3686
+ minute: "2-digit",
3687
+ hour12: true
3688
+ });
3650
3689
  }
3651
3690
  });
3652
3691
 
@@ -3686,7 +3725,7 @@ var init_formatters = __esm({
3686
3725
 
3687
3726
  // src/cli/ui.ts
3688
3727
  import { spinner } from "@clack/prompts";
3689
- import pc from "picocolors";
3728
+ import pc2 from "picocolors";
3690
3729
  async function withSpinner(message, fn) {
3691
3730
  if (!isTTY) {
3692
3731
  return fn();
@@ -3704,7 +3743,7 @@ async function withSpinner(message, fn) {
3704
3743
  }
3705
3744
  function showSummary(message) {
3706
3745
  if (!isTTY) return;
3707
- console.error(pc.dim(message));
3746
+ console.error(pc2.dim(message));
3708
3747
  }
3709
3748
  var isTTY;
3710
3749
  var init_ui = __esm({
@@ -3714,6 +3753,86 @@ var init_ui = __esm({
3714
3753
  }
3715
3754
  });
3716
3755
 
3756
+ // src/cli/validation.ts
3757
+ function validateSeason(raw) {
3758
+ const season = Number(raw);
3759
+ if (Number.isNaN(season) || !Number.isInteger(season)) {
3760
+ throw new Error(`Invalid season: "${raw}" \u2014 season must be a number (e.g. 2025)`);
3761
+ }
3762
+ if (season < 1897 || season > 2100) {
3763
+ throw new Error(`Invalid season: ${season} \u2014 must be between 1897 and 2100`);
3764
+ }
3765
+ return season;
3766
+ }
3767
+ function validateOptionalSeason(raw) {
3768
+ if (raw != null) return validateSeason(raw);
3769
+ return void 0;
3770
+ }
3771
+ function resolveDefaultSeason(competition = "AFLM") {
3772
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
3773
+ return competition === "AFLW" ? year - 1 : year;
3774
+ }
3775
+ function validateRound(raw) {
3776
+ const round = Number(raw);
3777
+ if (Number.isNaN(round) || !Number.isInteger(round) || round < 0) {
3778
+ throw new Error(`Invalid round: "${raw}" \u2014 round must be a non-negative integer`);
3779
+ }
3780
+ return round;
3781
+ }
3782
+ function validateFormat(raw) {
3783
+ if (raw == null) return void 0;
3784
+ const lower = raw.toLowerCase();
3785
+ if (VALID_FORMATS.includes(lower)) {
3786
+ return lower;
3787
+ }
3788
+ throw new Error(`Invalid format: "${raw}" \u2014 valid formats are: ${VALID_FORMATS.join(", ")}`);
3789
+ }
3790
+ function validateCompetition(raw) {
3791
+ const upper = raw.toUpperCase();
3792
+ if (upper === "AFLM" || upper === "AFLW") {
3793
+ return upper;
3794
+ }
3795
+ throw new Error(
3796
+ `Invalid competition: "${raw}" \u2014 valid values are: ${VALID_COMPETITIONS.join(", ")}`
3797
+ );
3798
+ }
3799
+ function validateOptionalCompetition(raw) {
3800
+ if (raw == null) return void 0;
3801
+ return validateCompetition(raw);
3802
+ }
3803
+ function validateSource(raw) {
3804
+ if (VALID_SOURCES.includes(raw)) {
3805
+ return raw;
3806
+ }
3807
+ throw new Error(`Invalid source: "${raw}" \u2014 valid sources are: ${VALID_SOURCES.join(", ")}`);
3808
+ }
3809
+ function resolveTeamIdentifier(raw, teams) {
3810
+ const trimmed = raw.trim();
3811
+ if (/^\d+$/.test(trimmed)) {
3812
+ return trimmed;
3813
+ }
3814
+ const canonical = normaliseTeamName(trimmed);
3815
+ const byCanonical = teams.find((t) => t.name === canonical);
3816
+ if (byCanonical) return byCanonical.teamId;
3817
+ const lower = trimmed.toLowerCase();
3818
+ const byName = teams.find((t) => t.name.toLowerCase() === lower);
3819
+ if (byName) return byName.teamId;
3820
+ const byAbbrev = teams.find((t) => t.abbreviation.toLowerCase() === lower);
3821
+ if (byAbbrev) return byAbbrev.teamId;
3822
+ const validNames = teams.map((t) => `${t.name} (${t.abbreviation})`).join(", ");
3823
+ throw new Error(`Unknown team: "${raw}" \u2014 valid teams are: ${validNames}`);
3824
+ }
3825
+ var VALID_SOURCES, VALID_COMPETITIONS, VALID_FORMATS;
3826
+ var init_validation2 = __esm({
3827
+ "src/cli/validation.ts"() {
3828
+ "use strict";
3829
+ init_team_mapping();
3830
+ VALID_SOURCES = ["afl-api", "footywire", "afl-tables", "squiggle"];
3831
+ VALID_COMPETITIONS = ["AFLM", "AFLW"];
3832
+ VALID_FORMATS = ["table", "json", "csv"];
3833
+ }
3834
+ });
3835
+
3717
3836
  // src/cli/commands/matches.ts
3718
3837
  var matches_exports = {};
3719
3838
  __export(matches_exports, {
@@ -3727,6 +3846,7 @@ var init_matches = __esm({
3727
3846
  init_index();
3728
3847
  init_formatters();
3729
3848
  init_ui();
3849
+ init_validation2();
3730
3850
  DEFAULT_COLUMNS = [
3731
3851
  { key: "date", label: "Date", maxWidth: 16 },
3732
3852
  { key: "roundNumber", label: "Round", maxWidth: 6 },
@@ -3756,16 +3876,14 @@ var init_matches = __esm({
3756
3876
  full: { type: "boolean", description: "Show all columns in table output" }
3757
3877
  },
3758
3878
  async run({ args }) {
3759
- const season = Number(args.season);
3760
- const round = args.round ? Number(args.round) : void 0;
3879
+ const season = validateSeason(args.season);
3880
+ const round = args.round ? validateRound(args.round) : void 0;
3881
+ const source = validateSource(args.source);
3882
+ const competition = validateCompetition(args.competition);
3883
+ const format = validateFormat(args.format);
3761
3884
  const result = await withSpinner(
3762
3885
  "Fetching match results\u2026",
3763
- () => fetchMatchResults({
3764
- source: args.source,
3765
- season,
3766
- round,
3767
- competition: args.competition
3768
- })
3886
+ () => fetchMatchResults({ source, season, round, competition })
3769
3887
  );
3770
3888
  if (!result.success) {
3771
3889
  throw result.error;
@@ -3775,7 +3893,7 @@ var init_matches = __esm({
3775
3893
  const formatOptions = {
3776
3894
  json: args.json,
3777
3895
  csv: args.csv,
3778
- format: args.format,
3896
+ format,
3779
3897
  full: args.full,
3780
3898
  columns: DEFAULT_COLUMNS
3781
3899
  };
@@ -3798,6 +3916,7 @@ var init_stats = __esm({
3798
3916
  init_index();
3799
3917
  init_formatters();
3800
3918
  init_ui();
3919
+ init_validation2();
3801
3920
  DEFAULT_COLUMNS2 = [
3802
3921
  { key: "displayName", label: "Player", maxWidth: 22 },
3803
3922
  { key: "team", label: "Team", maxWidth: 18 },
@@ -3828,18 +3947,15 @@ var init_stats = __esm({
3828
3947
  full: { type: "boolean", description: "Show all columns in table output" }
3829
3948
  },
3830
3949
  async run({ args }) {
3831
- const season = Number(args.season);
3832
- const round = args.round ? Number(args.round) : void 0;
3950
+ const season = validateSeason(args.season);
3951
+ const round = args.round ? validateRound(args.round) : void 0;
3833
3952
  const matchId = args["match-id"];
3953
+ const source = validateSource(args.source);
3954
+ const competition = validateCompetition(args.competition);
3955
+ const format = validateFormat(args.format);
3834
3956
  const result = await withSpinner(
3835
3957
  "Fetching player stats\u2026",
3836
- () => fetchPlayerStats({
3837
- source: args.source,
3838
- season,
3839
- round,
3840
- matchId,
3841
- competition: args.competition
3842
- })
3958
+ () => fetchPlayerStats({ source, season, round, matchId, competition })
3843
3959
  );
3844
3960
  if (!result.success) {
3845
3961
  throw result.error;
@@ -3851,7 +3967,7 @@ var init_stats = __esm({
3851
3967
  const formatOptions = {
3852
3968
  json: args.json,
3853
3969
  csv: args.csv,
3854
- format: args.format,
3970
+ format,
3855
3971
  full: args.full,
3856
3972
  columns: DEFAULT_COLUMNS2
3857
3973
  };
@@ -3874,6 +3990,7 @@ var init_fixture2 = __esm({
3874
3990
  init_index();
3875
3991
  init_formatters();
3876
3992
  init_ui();
3993
+ init_validation2();
3877
3994
  DEFAULT_COLUMNS3 = [
3878
3995
  { key: "roundNumber", label: "Round", maxWidth: 6 },
3879
3996
  { key: "date", label: "Date", maxWidth: 16 },
@@ -3901,16 +4018,14 @@ var init_fixture2 = __esm({
3901
4018
  full: { type: "boolean", description: "Show all columns in table output" }
3902
4019
  },
3903
4020
  async run({ args }) {
3904
- const season = Number(args.season);
3905
- const round = args.round ? Number(args.round) : void 0;
4021
+ const season = validateSeason(args.season);
4022
+ const round = args.round ? validateRound(args.round) : void 0;
4023
+ const source = validateSource(args.source);
4024
+ const competition = validateCompetition(args.competition);
4025
+ const format = validateFormat(args.format);
3906
4026
  const result = await withSpinner(
3907
4027
  "Fetching fixture\u2026",
3908
- () => fetchFixture({
3909
- source: args.source,
3910
- season,
3911
- round,
3912
- competition: args.competition
3913
- })
4028
+ () => fetchFixture({ source, season, round, competition })
3914
4029
  );
3915
4030
  if (!result.success) {
3916
4031
  throw result.error;
@@ -3920,7 +4035,7 @@ var init_fixture2 = __esm({
3920
4035
  const formatOptions = {
3921
4036
  json: args.json,
3922
4037
  csv: args.csv,
3923
- format: args.format,
4038
+ format,
3924
4039
  full: args.full,
3925
4040
  columns: DEFAULT_COLUMNS3
3926
4041
  };
@@ -3943,6 +4058,7 @@ var init_ladder3 = __esm({
3943
4058
  init_index();
3944
4059
  init_formatters();
3945
4060
  init_ui();
4061
+ init_validation2();
3946
4062
  DEFAULT_COLUMNS4 = [
3947
4063
  { key: "position", label: "Pos", maxWidth: 4 },
3948
4064
  { key: "team", label: "Team", maxWidth: 24 },
@@ -3972,16 +4088,14 @@ var init_ladder3 = __esm({
3972
4088
  full: { type: "boolean", description: "Show all columns in table output" }
3973
4089
  },
3974
4090
  async run({ args }) {
3975
- const season = Number(args.season);
3976
- const round = args.round ? Number(args.round) : void 0;
4091
+ const season = validateSeason(args.season);
4092
+ const round = args.round ? validateRound(args.round) : void 0;
4093
+ const source = validateSource(args.source);
4094
+ const competition = validateCompetition(args.competition);
4095
+ const format = validateFormat(args.format);
3977
4096
  const result = await withSpinner(
3978
4097
  "Fetching ladder\u2026",
3979
- () => fetchLadder({
3980
- source: args.source,
3981
- season,
3982
- round,
3983
- competition: args.competition
3984
- })
4098
+ () => fetchLadder({ source, season, round, competition })
3985
4099
  );
3986
4100
  if (!result.success) {
3987
4101
  throw result.error;
@@ -3993,7 +4107,7 @@ var init_ladder3 = __esm({
3993
4107
  const formatOptions = {
3994
4108
  json: args.json,
3995
4109
  csv: args.csv,
3996
- format: args.format,
4110
+ format,
3997
4111
  full: args.full,
3998
4112
  columns: DEFAULT_COLUMNS4
3999
4113
  };
@@ -4009,6 +4123,28 @@ __export(lineup_exports, {
4009
4123
  lineupCommand: () => lineupCommand
4010
4124
  });
4011
4125
  import { defineCommand as defineCommand5 } from "citty";
4126
+ function flattenLineups(lineups) {
4127
+ const rows = [];
4128
+ for (const lineup of lineups) {
4129
+ for (const { players, team } of [
4130
+ { players: lineup.homePlayers, team: lineup.homeTeam },
4131
+ { players: lineup.awayPlayers, team: lineup.awayTeam }
4132
+ ]) {
4133
+ for (const p of players) {
4134
+ rows.push({
4135
+ matchId: lineup.matchId,
4136
+ team,
4137
+ displayName: p.displayName,
4138
+ jumperNumber: p.jumperNumber,
4139
+ position: p.position,
4140
+ isEmergency: p.isEmergency,
4141
+ isSubstitute: p.isSubstitute
4142
+ });
4143
+ }
4144
+ }
4145
+ }
4146
+ return rows;
4147
+ }
4012
4148
  var DEFAULT_COLUMNS5, lineupCommand;
4013
4149
  var init_lineup3 = __esm({
4014
4150
  "src/cli/commands/lineup.ts"() {
@@ -4016,10 +4152,13 @@ var init_lineup3 = __esm({
4016
4152
  init_index();
4017
4153
  init_formatters();
4018
4154
  init_ui();
4155
+ init_validation2();
4019
4156
  DEFAULT_COLUMNS5 = [
4020
- { key: "matchId", label: "Match", maxWidth: 12 },
4021
- { key: "homeTeam", label: "Home", maxWidth: 20 },
4022
- { key: "awayTeam", label: "Away", maxWidth: 20 }
4157
+ { key: "matchId", label: "Match", maxWidth: 14 },
4158
+ { key: "team", label: "Team", maxWidth: 20 },
4159
+ { key: "displayName", label: "Player", maxWidth: 24 },
4160
+ { key: "jumperNumber", label: "#", maxWidth: 4 },
4161
+ { key: "position", label: "Pos", maxWidth: 12 }
4023
4162
  ];
4024
4163
  lineupCommand = defineCommand5({
4025
4164
  meta: {
@@ -4042,18 +4181,15 @@ var init_lineup3 = __esm({
4042
4181
  full: { type: "boolean", description: "Show all columns in table output" }
4043
4182
  },
4044
4183
  async run({ args }) {
4045
- const season = Number(args.season);
4046
- const round = Number(args.round);
4184
+ const season = validateSeason(args.season);
4185
+ const round = validateRound(args.round);
4047
4186
  const matchId = args["match-id"];
4187
+ const source = validateSource(args.source);
4188
+ const competition = validateCompetition(args.competition);
4189
+ const format = validateFormat(args.format);
4048
4190
  const result = await withSpinner(
4049
4191
  "Fetching lineups\u2026",
4050
- () => fetchLineup({
4051
- source: args.source,
4052
- season,
4053
- round,
4054
- matchId,
4055
- competition: args.competition
4056
- })
4192
+ () => fetchLineup({ source, season, round, matchId, competition })
4057
4193
  );
4058
4194
  if (!result.success) {
4059
4195
  throw result.error;
@@ -4063,11 +4199,16 @@ var init_lineup3 = __esm({
4063
4199
  const formatOptions = {
4064
4200
  json: args.json,
4065
4201
  csv: args.csv,
4066
- format: args.format,
4202
+ format,
4067
4203
  full: args.full,
4068
4204
  columns: DEFAULT_COLUMNS5
4069
4205
  };
4070
- console.log(formatOutput(data, formatOptions));
4206
+ const resolvedFormat = resolveFormat(formatOptions);
4207
+ if (resolvedFormat === "json") {
4208
+ console.log(formatOutput(data, formatOptions));
4209
+ } else {
4210
+ console.log(formatOutput(flattenLineups(data), formatOptions));
4211
+ }
4071
4212
  }
4072
4213
  });
4073
4214
  }
@@ -4086,6 +4227,7 @@ var init_squad = __esm({
4086
4227
  init_index();
4087
4228
  init_formatters();
4088
4229
  init_ui();
4230
+ init_validation2();
4089
4231
  DEFAULT_COLUMNS6 = [
4090
4232
  { key: "displayName", label: "Player", maxWidth: 24 },
4091
4233
  { key: "jumperNumber", label: "#", maxWidth: 4 },
@@ -4099,7 +4241,11 @@ var init_squad = __esm({
4099
4241
  description: "Fetch team squad for a season"
4100
4242
  },
4101
4243
  args: {
4102
- "team-id": { type: "string", description: "Team ID", required: true },
4244
+ "team-id": {
4245
+ type: "string",
4246
+ description: "Team ID, abbreviation, or name (e.g. 5, CARL, Carlton)",
4247
+ required: true
4248
+ },
4103
4249
  season: { type: "string", description: "Season year (e.g. 2025)", required: true },
4104
4250
  competition: {
4105
4251
  type: "string",
@@ -4112,15 +4258,21 @@ var init_squad = __esm({
4112
4258
  full: { type: "boolean", description: "Show all columns in table output" }
4113
4259
  },
4114
4260
  async run({ args }) {
4115
- const teamId = args["team-id"];
4116
- const season = Number(args.season);
4261
+ const season = validateSeason(args.season);
4262
+ const competition = validateCompetition(args.competition);
4263
+ const format = validateFormat(args.format);
4264
+ let teamId = args["team-id"].trim();
4265
+ const isNumeric = /^\d+$/.test(teamId);
4266
+ if (!isNumeric) {
4267
+ const teamsResult = await withSpinner("Resolving team\u2026", () => fetchTeams({ competition }));
4268
+ if (!teamsResult.success) {
4269
+ throw teamsResult.error;
4270
+ }
4271
+ teamId = resolveTeamIdentifier(args["team-id"], teamsResult.data);
4272
+ }
4117
4273
  const result = await withSpinner(
4118
4274
  "Fetching squad\u2026",
4119
- () => fetchSquad({
4120
- teamId,
4121
- season,
4122
- competition: args.competition
4123
- })
4275
+ () => fetchSquad({ teamId, season, competition })
4124
4276
  );
4125
4277
  if (!result.success) {
4126
4278
  throw result.error;
@@ -4130,7 +4282,7 @@ var init_squad = __esm({
4130
4282
  const formatOptions = {
4131
4283
  json: args.json,
4132
4284
  csv: args.csv,
4133
- format: args.format,
4285
+ format,
4134
4286
  full: args.full,
4135
4287
  columns: DEFAULT_COLUMNS6
4136
4288
  };
@@ -4153,6 +4305,7 @@ var init_teams2 = __esm({
4153
4305
  init_index();
4154
4306
  init_formatters();
4155
4307
  init_ui();
4308
+ init_validation2();
4156
4309
  DEFAULT_COLUMNS7 = [
4157
4310
  { key: "teamId", label: "ID", maxWidth: 8 },
4158
4311
  { key: "name", label: "Team", maxWidth: 24 },
@@ -4173,22 +4326,26 @@ var init_teams2 = __esm({
4173
4326
  full: { type: "boolean", description: "Show all columns in table output" }
4174
4327
  },
4175
4328
  async run({ args }) {
4329
+ const competition = validateOptionalCompetition(args.competition);
4330
+ const format = validateFormat(args.format);
4176
4331
  const result = await withSpinner(
4177
4332
  "Fetching teams\u2026",
4178
- () => fetchTeams({
4179
- competition: args.competition,
4180
- teamType: args["team-type"]
4181
- })
4333
+ () => fetchTeams({ competition, teamType: args["team-type"] })
4182
4334
  );
4183
4335
  if (!result.success) {
4184
4336
  throw result.error;
4185
4337
  }
4186
4338
  const data = result.data;
4339
+ if (data.length === 0 && args["team-type"]) {
4340
+ console.error(
4341
+ `No teams found for team type "${args["team-type"]}". Try running without --team-type to see available teams.`
4342
+ );
4343
+ }
4187
4344
  showSummary(`Loaded ${data.length} teams`);
4188
4345
  const formatOptions = {
4189
4346
  json: args.json,
4190
4347
  csv: args.csv,
4191
- format: args.format,
4348
+ format,
4192
4349
  full: args.full,
4193
4350
  columns: DEFAULT_COLUMNS7
4194
4351
  };
@@ -4217,9 +4374,18 @@ var init_team_stats2 = __esm({
4217
4374
  init_index();
4218
4375
  init_formatters();
4219
4376
  init_ui();
4377
+ init_validation2();
4220
4378
  DEFAULT_COLUMNS8 = [
4221
4379
  { key: "team", label: "Team", maxWidth: 24 },
4222
- { key: "gamesPlayed", label: "GP", maxWidth: 5 }
4380
+ { key: "gamesPlayed", label: "GP", maxWidth: 5 },
4381
+ { key: "K", label: "K", maxWidth: 6 },
4382
+ { key: "HB", label: "HB", maxWidth: 6 },
4383
+ { key: "D", label: "D", maxWidth: 6 },
4384
+ { key: "M", label: "M", maxWidth: 6 },
4385
+ { key: "G", label: "G", maxWidth: 6 },
4386
+ { key: "B", label: "B", maxWidth: 6 },
4387
+ { key: "T", label: "T", maxWidth: 6 },
4388
+ { key: "I50", label: "I50", maxWidth: 6 }
4223
4389
  ];
4224
4390
  teamStatsCommand = defineCommand8({
4225
4391
  meta: {
@@ -4240,15 +4406,13 @@ var init_team_stats2 = __esm({
4240
4406
  full: { type: "boolean", description: "Show all columns in table output" }
4241
4407
  },
4242
4408
  async run({ args }) {
4243
- const season = Number(args.season);
4409
+ const season = validateSeason(args.season);
4410
+ const source = validateSource(args.source);
4411
+ const format = validateFormat(args.format);
4244
4412
  const summaryType = args.summary;
4245
4413
  const result = await withSpinner(
4246
4414
  "Fetching team stats\u2026",
4247
- () => fetchTeamStats({
4248
- source: args.source,
4249
- season,
4250
- summaryType
4251
- })
4415
+ () => fetchTeamStats({ source, season, summaryType })
4252
4416
  );
4253
4417
  if (!result.success) {
4254
4418
  throw result.error;
@@ -4259,7 +4423,7 @@ var init_team_stats2 = __esm({
4259
4423
  const formatOptions = {
4260
4424
  json: args.json,
4261
4425
  csv: args.csv,
4262
- format: args.format,
4426
+ format,
4263
4427
  full: args.full,
4264
4428
  columns: DEFAULT_COLUMNS8
4265
4429
  };
@@ -4282,6 +4446,7 @@ var init_player_details2 = __esm({
4282
4446
  init_index();
4283
4447
  init_formatters();
4284
4448
  init_ui();
4449
+ init_validation2();
4285
4450
  DEFAULT_COLUMNS9 = [
4286
4451
  { key: "displayName", label: "Player", maxWidth: 24 },
4287
4452
  { key: "jumperNumber", label: "#", maxWidth: 4 },
@@ -4315,16 +4480,13 @@ var init_player_details2 = __esm({
4315
4480
  full: { type: "boolean", description: "Show all columns in table output" }
4316
4481
  },
4317
4482
  async run({ args }) {
4318
- const source = args.source;
4319
- const season = args.season ? Number(args.season) : void 0;
4483
+ const source = validateSource(args.source);
4484
+ const competition = validateCompetition(args.competition);
4485
+ const format = validateFormat(args.format);
4486
+ const season = validateOptionalSeason(args.season) ?? resolveDefaultSeason(competition);
4320
4487
  const result = await withSpinner(
4321
4488
  "Fetching player details\u2026",
4322
- () => fetchPlayerDetails({
4323
- source,
4324
- team: args.team,
4325
- season,
4326
- competition: args.competition
4327
- })
4489
+ () => fetchPlayerDetails({ source, team: args.team, season, competition })
4328
4490
  );
4329
4491
  if (!result.success) {
4330
4492
  throw result.error;
@@ -4334,7 +4496,7 @@ var init_player_details2 = __esm({
4334
4496
  const formatOptions = {
4335
4497
  json: args.json,
4336
4498
  csv: args.csv,
4337
- format: args.format,
4499
+ format,
4338
4500
  full: args.full,
4339
4501
  columns: DEFAULT_COLUMNS9
4340
4502
  };
@@ -4357,6 +4519,7 @@ var init_coaches_votes2 = __esm({
4357
4519
  init_index();
4358
4520
  init_formatters();
4359
4521
  init_ui();
4522
+ init_validation2();
4360
4523
  DEFAULT_COLUMNS10 = [
4361
4524
  { key: "season", label: "Season", maxWidth: 8 },
4362
4525
  { key: "round", label: "Round", maxWidth: 6 },
@@ -4385,16 +4548,13 @@ var init_coaches_votes2 = __esm({
4385
4548
  full: { type: "boolean", description: "Show all columns in table output" }
4386
4549
  },
4387
4550
  async run({ args }) {
4388
- const season = Number(args.season);
4389
- const round = args.round ? Number(args.round) : void 0;
4551
+ const season = validateSeason(args.season);
4552
+ const round = args.round ? validateRound(args.round) : void 0;
4553
+ const competition = validateCompetition(args.competition);
4554
+ const format = validateFormat(args.format);
4390
4555
  const result = await withSpinner(
4391
4556
  "Fetching coaches votes\u2026",
4392
- () => fetchCoachesVotes({
4393
- season,
4394
- round,
4395
- competition: args.competition,
4396
- team: args.team
4397
- })
4557
+ () => fetchCoachesVotes({ season, round, competition, team: args.team })
4398
4558
  );
4399
4559
  if (!result.success) {
4400
4560
  throw result.error;
@@ -4406,7 +4566,7 @@ var init_coaches_votes2 = __esm({
4406
4566
  const formatOptions = {
4407
4567
  json: args.json,
4408
4568
  csv: args.csv,
4409
- format: args.format,
4569
+ format,
4410
4570
  full: args.full,
4411
4571
  columns: DEFAULT_COLUMNS10
4412
4572
  };
@@ -4417,13 +4577,39 @@ var init_coaches_votes2 = __esm({
4417
4577
  });
4418
4578
 
4419
4579
  // src/cli.ts
4420
- init_errors();
4421
4580
  import { defineCommand as defineCommand11, runMain } from "citty";
4422
- import pc2 from "picocolors";
4581
+
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}`;
4604
+ }
4605
+ return `${pc.red("Error:")} ${String(error)}`;
4606
+ }
4607
+
4608
+ // src/cli.ts
4423
4609
  var main = defineCommand11({
4424
4610
  meta: {
4425
4611
  name: "fitzroy",
4426
- version: "1.1.0",
4612
+ version: "1.1.1",
4427
4613
  description: "CLI for fetching AFL data \u2014 match results, player stats, fixtures, ladders, and more"
4428
4614
  },
4429
4615
  subCommands: {
@@ -4439,28 +4625,6 @@ var main = defineCommand11({
4439
4625
  "coaches-votes": () => Promise.resolve().then(() => (init_coaches_votes2(), coaches_votes_exports)).then((m) => m.coachesVotesCommand)
4440
4626
  }
4441
4627
  });
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
4628
  runMain(main).catch((error) => {
4465
4629
  console.error(formatError(error));
4466
4630
  process.exit(1);