fifa-wc26 0.1.2 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +6 -5
  2. package/dist/cli.js +311 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ The package name is `fifa-wc26`; the binary it installs is `wc26`.
14
14
 
15
15
  ## Quick start
16
16
 
17
- No key required for the default providers (`espn`, `thesportsdb`).
17
+ Default providers work keyless. No user-side key required.
18
18
 
19
19
  ```bash
20
20
  wc26 fixtures --team ARG --from today
@@ -38,15 +38,16 @@ export WC26_THESPORTSDB_KEY=...
38
38
  - `--json` — JSON to stdout
39
39
  - `--plain` — TSV, no color
40
40
  - `--no-cache` — skip cache read
41
- - `--provider <name>` — force a specific provider (`espn`, `thesportsdb`)
41
+ - `--provider <name>` — force a specific provider (`football-data`, `espn`, `thesportsdb`)
42
42
  - `--verbose` — include stack traces on error
43
43
 
44
44
  ## Providers
45
45
 
46
- `wc26` chains keyless providers with automatic failover:
46
+ `wc26` chains providers with automatic failover:
47
47
 
48
- 1. `espn` (site.api.espn.comkeyless)
49
- 2. `thesportsdb` (thesportsdb.com — free tier uses key `3`; set a paid key for higher limits)
48
+ 1. `football-data` (api.football-data.orgfull 104-match WC schedule + knockout stages; no key needed)
49
+ 2. `espn` (site.api.espn.com — keyless)
50
+ 3. `thesportsdb` (thesportsdb.com — free tier uses key `3`; set a paid key for higher limits)
50
51
 
51
52
  Set a paid `thesportsdb` key via env var (`WC26_THESPORTSDB_KEY`) or `wc26 config set apiKey thesportsdb <key>`.
52
53
 
package/dist/cli.js CHANGED
@@ -102,7 +102,7 @@ init_esm_shims();
102
102
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
103
103
  import { join } from "path";
104
104
  var DEFAULTS = {
105
- providers: ["espn", "thesportsdb"],
105
+ providers: ["football-data", "espn", "thesportsdb"],
106
106
  apiKeys: {},
107
107
  defaults: { watchIntervalSec: 15, output: "pretty" }
108
108
  };
@@ -671,6 +671,308 @@ var TheSportsDbProvider = class {
671
671
  }
672
672
  };
673
673
 
674
+ // src/providers/football-data.ts
675
+ init_esm_shims();
676
+
677
+ // src/data/wc26-venues.ts
678
+ init_esm_shims();
679
+ var WC26_VENUES = {
680
+ "537327": "Estadio Azteca, Mexico City",
681
+ "537328": "Estadio Akron, Guadalajara",
682
+ "537329": "Mercedes-Benz Stadium, Atlanta",
683
+ "537330": "Estadio Akron, Guadalajara",
684
+ "537331": "Estadio Azteca, Mexico City",
685
+ "537332": "Estadio BBVA, Monterrey",
686
+ "537333": "BMO Field, Toronto",
687
+ "537334": "Levi's Stadium, Bay Area",
688
+ "537335": "SoFi Stadium, Los Angeles",
689
+ "537336": "BC Place, Vancouver",
690
+ "537337": "BC Place, Vancouver",
691
+ "537338": "Lumen Field, Seattle",
692
+ "537339": "MetLife Stadium, New York/New Jersey",
693
+ "537340": "Gillette Stadium, Boston",
694
+ "537341": "Lincoln Financial Field, Philadelphia",
695
+ "537342": "Gillette Stadium, Boston",
696
+ "537343": "Hard Rock Stadium, Miami",
697
+ "537344": "Mercedes-Benz Stadium, Atlanta",
698
+ "537345": "SoFi Stadium, Los Angeles",
699
+ "537346": "BC Place, Vancouver",
700
+ "537347": "Levi's Stadium, Bay Area",
701
+ "537348": "Lumen Field, Seattle",
702
+ "537349": "SoFi Stadium, Los Angeles",
703
+ "537350": "Levi's Stadium, Bay Area",
704
+ "537351": "NRG Stadium, Houston",
705
+ "537352": "Lincoln Financial Field, Philadelphia",
706
+ "537353": "BMO Field, Toronto",
707
+ "537354": "Arrowhead Stadium, Kansas City",
708
+ "537355": "MetLife Stadium, New York/New Jersey",
709
+ "537356": "Lincoln Financial Field, Philadelphia",
710
+ "537357": "AT&T Stadium, Dallas",
711
+ "537358": "Estadio BBVA, Monterrey",
712
+ "537359": "NRG Stadium, Houston",
713
+ "537360": "Estadio BBVA, Monterrey",
714
+ "537361": "Arrowhead Stadium, Kansas City",
715
+ "537362": "AT&T Stadium, Dallas",
716
+ "537363": "Lumen Field, Seattle",
717
+ "537364": "SoFi Stadium, Los Angeles",
718
+ "537365": "SoFi Stadium, Los Angeles",
719
+ "537366": "BC Place, Vancouver",
720
+ "537367": "BC Place, Vancouver",
721
+ "537368": "Lumen Field, Seattle",
722
+ "537369": "Mercedes-Benz Stadium, Atlanta",
723
+ "537370": "Hard Rock Stadium, Miami",
724
+ "537371": "Mercedes-Benz Stadium, Atlanta",
725
+ "537372": "Hard Rock Stadium, Miami",
726
+ "537373": "Estadio Akron, Guadalajara",
727
+ "537374": "NRG Stadium, Houston",
728
+ "537375": "Lincoln Financial Field, Philadelphia",
729
+ "537376": "NRG Stadium, Houston",
730
+ "537377": "MetLife Stadium, New York/New Jersey",
731
+ "537378": "Estadio Azteca, Mexico City",
732
+ "537379": "AT&T Stadium, Dallas",
733
+ "537380": "Lumen Field, Seattle",
734
+ "537381": "Mercedes-Benz Stadium, Atlanta",
735
+ "537382": "BC Place, Vancouver",
736
+ "537383": "Gillette Stadium, Boston",
737
+ "537384": "SoFi Stadium, Los Angeles",
738
+ "537385": "Hard Rock Stadium, Miami",
739
+ "537386": "Arrowhead Stadium, Kansas City",
740
+ "537387": "AT&T Stadium, Dallas",
741
+ "537388": "Mercedes-Benz Stadium, Atlanta",
742
+ "537389": "Hard Rock Stadium, Miami",
743
+ "537390": "MetLife Stadium, New York/New Jersey",
744
+ "537391": "MetLife Stadium, New York/New Jersey",
745
+ "537392": "Gillette Stadium, Boston",
746
+ "537393": "Lincoln Financial Field, Philadelphia",
747
+ "537394": "MetLife Stadium, New York/New Jersey",
748
+ "537395": "Gillette Stadium, Boston",
749
+ "537396": "BMO Field, Toronto",
750
+ "537397": "Arrowhead Stadium, Kansas City",
751
+ "537398": "Levi's Stadium, Bay Area",
752
+ "537399": "AT&T Stadium, Dallas",
753
+ "537400": "Levi's Stadium, Bay Area",
754
+ "537401": "AT&T Stadium, Dallas",
755
+ "537402": "Arrowhead Stadium, Kansas City",
756
+ "537403": "NRG Stadium, Houston",
757
+ "537404": "Estadio Azteca, Mexico City",
758
+ "537405": "NRG Stadium, Houston",
759
+ "537406": "Estadio Akron, Guadalajara",
760
+ "537407": "Hard Rock Stadium, Miami",
761
+ "537408": "Mercedes-Benz Stadium, Atlanta",
762
+ "537409": "AT&T Stadium, Dallas",
763
+ "537410": "BMO Field, Toronto",
764
+ "537411": "Gillette Stadium, Boston",
765
+ "537412": "BMO Field, Toronto",
766
+ "537413": "MetLife Stadium, New York/New Jersey",
767
+ "537414": "Lincoln Financial Field, Philadelphia",
768
+ "537415": "Gillette Stadium, Boston",
769
+ "537416": "MetLife Stadium, New York/New Jersey",
770
+ "537417": "SoFi Stadium, Los Angeles",
771
+ "537418": "Estadio BBVA, Monterrey",
772
+ "537419": "BMO Field, Toronto",
773
+ "537420": "SoFi Stadium, Los Angeles",
774
+ "537421": "Levi's Stadium, Bay Area",
775
+ "537422": "Lumen Field, Seattle",
776
+ "537423": "NRG Stadium, Houston",
777
+ "537424": "AT&T Stadium, Dallas",
778
+ "537425": "Estadio Azteca, Mexico City",
779
+ "537426": "Mercedes-Benz Stadium, Atlanta",
780
+ "537427": "Hard Rock Stadium, Miami",
781
+ "537428": "AT&T Stadium, Dallas",
782
+ "537429": "BC Place, Vancouver",
783
+ "537430": "Arrowhead Stadium, Kansas City"
784
+ };
785
+ function lookupWC26Venue(matchId) {
786
+ return WC26_VENUES[String(matchId)];
787
+ }
788
+
789
+ // src/providers/football-data.ts
790
+ var DEFAULT_PROXY_BASE = "https://wc26-proxy.vercel.app";
791
+ function resolveBase(override) {
792
+ return (override ?? process.env.WC26_PROXY_URL ?? DEFAULT_PROXY_BASE).replace(/\/+$/, "");
793
+ }
794
+ var STAGE_MAP = {
795
+ GROUP_STAGE: "group",
796
+ LAST_16: "r16",
797
+ QUARTER_FINALS: "qf",
798
+ SEMI_FINALS: "sf",
799
+ THIRD_PLACE: "third",
800
+ FINAL: "final"
801
+ };
802
+ function mapStage3(s) {
803
+ return STAGE_MAP[s ?? ""] ?? "group";
804
+ }
805
+ function mapStatus3(s) {
806
+ switch (s) {
807
+ case "LIVE":
808
+ case "IN_PLAY":
809
+ case "PAUSED":
810
+ return "live";
811
+ case "FINISHED":
812
+ case "AWARDED":
813
+ return "finished";
814
+ case "POSTPONED":
815
+ case "SUSPENDED":
816
+ return "postponed";
817
+ case "CANCELLED":
818
+ return "cancelled";
819
+ default:
820
+ return "scheduled";
821
+ }
822
+ }
823
+ function teamRef2(t) {
824
+ const name = t.name ?? t.shortName ?? t.tla ?? "?";
825
+ const code = (t.tla ?? name.slice(0, 3)).toUpperCase().slice(0, 3);
826
+ return { code, name };
827
+ }
828
+ function fixtureFromMatch(m) {
829
+ const status = mapStatus3(m.status);
830
+ const ft = m.score?.fullTime;
831
+ const hs = ft?.home;
832
+ const as = ft?.away;
833
+ const score = hs != null && as != null && (status === "live" || status === "finished") ? {
834
+ home: hs,
835
+ away: as,
836
+ homePens: m.score?.penalties?.home ?? void 0,
837
+ awayPens: m.score?.penalties?.away ?? void 0
838
+ } : void 0;
839
+ const group = m.group ? m.group.replace(/^GROUP[_\s]+/i, "").trim().toUpperCase().slice(0, 1) : void 0;
840
+ const id = String(m.id);
841
+ return {
842
+ id,
843
+ utcKickoff: new Date(m.utcDate).toISOString(),
844
+ stage: mapStage3(m.stage),
845
+ group: group || void 0,
846
+ home: teamRef2(m.homeTeam),
847
+ away: teamRef2(m.awayTeam),
848
+ venue: m.venue ?? lookupWC26Venue(id) ?? "",
849
+ status,
850
+ score
851
+ };
852
+ }
853
+ var FootballDataProvider = class {
854
+ name = "football-data";
855
+ base;
856
+ constructor(proxyBase) {
857
+ this.base = resolveBase(proxyBase);
858
+ }
859
+ isConfigured() {
860
+ return Boolean(this.base);
861
+ }
862
+ url(path2, qs) {
863
+ const q = qs && [...qs].length ? `?${qs.toString()}` : "";
864
+ return `${this.base}${path2}${q}`;
865
+ }
866
+ async matches(q = {}) {
867
+ const params = new URLSearchParams();
868
+ if (q.from) params.set("dateFrom", q.from);
869
+ if (q.to) params.set("dateTo", q.to);
870
+ const data = await httpJson(this.url("/competitions/WC/matches", params));
871
+ return data.matches ?? [];
872
+ }
873
+ async fixtures(q) {
874
+ const raw = await this.matches({ from: q.from, to: q.to });
875
+ let out = raw.map(fixtureFromMatch);
876
+ if (q.teamCode) out = out.filter((f) => f.home.code === q.teamCode || f.away.code === q.teamCode);
877
+ if (q.stage) out = out.filter((f) => f.stage === q.stage);
878
+ return out;
879
+ }
880
+ async liveMatches() {
881
+ const raw = await this.matches();
882
+ return raw.map(fixtureFromMatch).filter((f) => f.status === "live").map((f) => ({ ...f, minute: 0, events: [] }));
883
+ }
884
+ async match(id) {
885
+ const data = await httpJson(this.url(`/matches/${id}`));
886
+ if (!data.id || !data.utcDate || !data.homeTeam || !data.awayTeam || !data.status) {
887
+ throw new WC26Error("NOT_FOUND", `match ${id} not found`);
888
+ }
889
+ const m = {
890
+ id: data.id,
891
+ utcDate: data.utcDate,
892
+ status: data.status,
893
+ homeTeam: data.homeTeam,
894
+ awayTeam: data.awayTeam,
895
+ score: data.score,
896
+ venue: data.venue,
897
+ stage: data.stage,
898
+ group: data.group
899
+ };
900
+ const f = fixtureFromMatch(m);
901
+ const ref = data.referees?.find((r) => /referee/i.test(r.role ?? ""))?.name;
902
+ return {
903
+ ...f,
904
+ minute: 0,
905
+ events: [],
906
+ lineups: { home: [], away: [] },
907
+ stats: {},
908
+ referee: ref
909
+ };
910
+ }
911
+ async standings(group) {
912
+ const data = await httpJson(this.url("/competitions/WC/standings"));
913
+ const out = [];
914
+ for (const s of data.standings ?? []) {
915
+ if (s.type && s.type !== "TOTAL") continue;
916
+ const label = (s.group ?? "").replace(/^GROUP_/i, "").trim().toUpperCase().slice(0, 1);
917
+ if (!label) continue;
918
+ out.push({
919
+ group: label,
920
+ rows: (s.table ?? []).map((r) => ({
921
+ team: teamRef2(r.team),
922
+ p: r.playedGames,
923
+ w: r.won,
924
+ d: r.draw,
925
+ l: r.lost,
926
+ gf: r.goalsFor,
927
+ ga: r.goalsAgainst,
928
+ gd: r.goalDifference,
929
+ pts: r.points
930
+ }))
931
+ });
932
+ }
933
+ out.sort((a, b) => a.group.localeCompare(b.group));
934
+ return group ? out.filter((g) => g.group === group.toUpperCase()) : out;
935
+ }
936
+ async knockoutBracket() {
937
+ const fixtures = await this.fixtures({});
938
+ const stages = ["r16", "qf", "sf", "third", "final"];
939
+ return fixtures.filter((f) => stages.includes(f.stage)).map((f) => ({
940
+ stage: f.stage,
941
+ matchId: f.id,
942
+ home: f.home,
943
+ away: f.away,
944
+ winner: f.status === "finished" && f.score ? f.score.home > f.score.away ? f.home : f.score.away > f.score.home ? f.away : void 0 : void 0
945
+ }));
946
+ }
947
+ async team(code) {
948
+ const data = await httpJson(this.url("/competitions/WC/teams"));
949
+ const list = data.teams ?? [];
950
+ const match = list.find((t) => (t.tla ?? "").toUpperCase() === code.toUpperCase());
951
+ if (!match) throw new WC26Error("NOT_FOUND", `team ${code} not found`);
952
+ let squad = [];
953
+ let coach = match.coach?.name;
954
+ try {
955
+ const t = await httpJson(this.url(`/teams/${match.id}`));
956
+ squad = (t.squad ?? []).map((p) => ({
957
+ name: p.name ?? "?",
958
+ position: p.position ?? "",
959
+ number: p.shirtNumber ?? void 0
960
+ }));
961
+ coach = t.coach?.name ?? coach;
962
+ } catch {
963
+ squad = [];
964
+ }
965
+ const fixtures = await this.fixtures({ teamCode: code });
966
+ return {
967
+ code: (match.tla ?? code).toUpperCase(),
968
+ name: match.name,
969
+ coach,
970
+ squad,
971
+ fixtures
972
+ };
973
+ }
974
+ };
975
+
674
976
  // src/commands/_shared.ts
675
977
  function homeDir() {
676
978
  return process.env.WC26_HOME ?? join3(homedir(), ".wc26");
@@ -691,6 +993,10 @@ async function buildRegistry(opts) {
691
993
  const key = await env.config.apiKey(name) ?? "";
692
994
  if (name === "espn") providers.push(new EspnProvider());
693
995
  else if (name === "thesportsdb") providers.push(new TheSportsDbProvider(key || "3"));
996
+ else if (name === "football-data") {
997
+ const p = new FootballDataProvider();
998
+ if (p.isConfigured()) providers.push(p);
999
+ }
694
1000
  }
695
1001
  return new ProviderRegistry(providers);
696
1002
  }
@@ -793,7 +1099,8 @@ var colorStatus = (s) => {
793
1099
  return chalk.cyan(s);
794
1100
  };
795
1101
  var scoreStr2 = (f) => f.score ? `${f.score.home}-${f.score.away}` : "\u2013";
796
- function renderFixturesPretty(fixtures) {
1102
+ function renderFixturesPretty(fixtures, emptyMessage = "no matches") {
1103
+ if (fixtures.length === 0) return chalk.dim(emptyMessage);
797
1104
  const t = new Table({
798
1105
  head: ["Kickoff (UTC)", "Stage", "Home", "Away", "Venue", "Score", "Status"],
799
1106
  style: { head: ["bold"] }
@@ -916,7 +1223,7 @@ function nextCmd(p) {
916
1223
  else {
917
1224
  if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
918
1225
  `);
919
- process.stdout.write(renderFixturesPretty(out) + "\n");
1226
+ process.stdout.write(renderFixturesPretty(out, "no upcoming matches") + "\n");
920
1227
  }
921
1228
  } catch (e) {
922
1229
  die(e, g);
@@ -1282,7 +1589,7 @@ function cacheCmd(p) {
1282
1589
 
1283
1590
  // src/cli.ts
1284
1591
  var program = new Command();
1285
- program.name("wc26").description("FIFA World Cup 2026 CLI").version("0.1.0").option("--json", "emit JSON to stdout").option("--plain", "tab-separated, no color").option("--no-cache", "skip cache read").option("--provider <name>", "force a specific provider").option("--verbose", "include stack traces");
1592
+ program.name("wc26").description("FIFA World Cup 2026 CLI").version("0.1.2").option("--json", "emit JSON to stdout").option("--plain", "tab-separated, no color").option("--no-cache", "skip cache read").option("--provider <name>", "force a specific provider").option("--verbose", "include stack traces");
1286
1593
  fixturesCmd(program);
1287
1594
  nextCmd(program);
1288
1595
  liveCmd(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fifa-wc26",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "FIFA World Cup 2026 CLI: fixtures, live scores, group standings, ASCII bracket, team lookup. Keyless providers, smart cache, terminal-friendly output.",
5
5
  "type": "module",
6
6
  "bin": {