fifa-wc26 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +27 -5
  2. package/dist/cli.js +196 -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 (`espn`, `thesportsdb`). The bundled `football-data` provider talks to a project-hosted proxy (no user-side key required); see [Providers](#providers) for proxy details.
18
18
 
19
19
  ```bash
20
20
  wc26 fixtures --team ARG --from today
@@ -38,18 +38,40 @@ 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.org via the project proxy full 104-match WC schedule + knockout stages; activates when a proxy URL is baked into the build or `WC26_PROXY_URL` is set)
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
 
54
+ ### football-data proxy
55
+
56
+ The football-data.org API token is **not** shipped to end users. Requests are
57
+ forwarded through a small Vercel Edge Function (`proxy/vercel/`) that attaches
58
+ the token server-side and CDN-caches responses to amortize the 10 req/min
59
+ upstream limit.
60
+
61
+ Maintainers: deploy the proxy, then set the resulting URL once before
62
+ publishing:
63
+
64
+ ```bash
65
+ cd proxy/vercel
66
+ npm install && npx vercel login && npx vercel link
67
+ npx vercel env add FOOTBALL_DATA_TOKEN production # paste token when prompted
68
+ npm run deploy # prints https://<project>.vercel.app
69
+ ```
70
+
71
+ Then edit `DEFAULT_PROXY_BASE` in `src/providers/football-data.ts` to that URL
72
+ and `npm run build`. End users get the proxied provider automatically; no key,
73
+ no signup. Override at runtime with `WC26_PROXY_URL=...`.
74
+
53
75
  ## Cache
54
76
 
55
77
  JSON files under `~/.wc26/cache/`. TTLs: fixtures 6 h, standings 1 h, bracket 1 h, finished match 24 h, live 10 s. On provider failure the CLI serves stale cache and tags output `[STALE]` (or `"stale": true` in JSON).
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,193 @@ var TheSportsDbProvider = class {
671
671
  }
672
672
  };
673
673
 
674
+ // src/providers/football-data.ts
675
+ init_esm_shims();
676
+ var DEFAULT_PROXY_BASE = "https://wc26-proxy.vercel.app";
677
+ function resolveBase(override) {
678
+ return (override ?? process.env.WC26_PROXY_URL ?? DEFAULT_PROXY_BASE).replace(/\/+$/, "");
679
+ }
680
+ var STAGE_MAP = {
681
+ GROUP_STAGE: "group",
682
+ LAST_16: "r16",
683
+ QUARTER_FINALS: "qf",
684
+ SEMI_FINALS: "sf",
685
+ THIRD_PLACE: "third",
686
+ FINAL: "final"
687
+ };
688
+ function mapStage3(s) {
689
+ return STAGE_MAP[s ?? ""] ?? "group";
690
+ }
691
+ function mapStatus3(s) {
692
+ switch (s) {
693
+ case "LIVE":
694
+ case "IN_PLAY":
695
+ case "PAUSED":
696
+ return "live";
697
+ case "FINISHED":
698
+ case "AWARDED":
699
+ return "finished";
700
+ case "POSTPONED":
701
+ case "SUSPENDED":
702
+ return "postponed";
703
+ case "CANCELLED":
704
+ return "cancelled";
705
+ default:
706
+ return "scheduled";
707
+ }
708
+ }
709
+ function teamRef2(t) {
710
+ const name = t.name ?? t.shortName ?? t.tla ?? "?";
711
+ const code = (t.tla ?? name.slice(0, 3)).toUpperCase().slice(0, 3);
712
+ return { code, name };
713
+ }
714
+ function fixtureFromMatch(m) {
715
+ const status = mapStatus3(m.status);
716
+ const ft = m.score?.fullTime;
717
+ const hs = ft?.home;
718
+ const as = ft?.away;
719
+ const score = hs != null && as != null && (status === "live" || status === "finished") ? {
720
+ home: hs,
721
+ away: as,
722
+ homePens: m.score?.penalties?.home ?? void 0,
723
+ awayPens: m.score?.penalties?.away ?? void 0
724
+ } : void 0;
725
+ const group = m.group ? m.group.replace(/^Group\s+/i, "").trim().toUpperCase().slice(0, 1) : void 0;
726
+ return {
727
+ id: String(m.id),
728
+ utcKickoff: new Date(m.utcDate).toISOString(),
729
+ stage: mapStage3(m.stage),
730
+ group: group || void 0,
731
+ home: teamRef2(m.homeTeam),
732
+ away: teamRef2(m.awayTeam),
733
+ venue: m.venue ?? "",
734
+ status,
735
+ score
736
+ };
737
+ }
738
+ var FootballDataProvider = class {
739
+ name = "football-data";
740
+ base;
741
+ constructor(proxyBase) {
742
+ this.base = resolveBase(proxyBase);
743
+ }
744
+ isConfigured() {
745
+ return Boolean(this.base);
746
+ }
747
+ url(path2, qs) {
748
+ const q = qs && [...qs].length ? `?${qs.toString()}` : "";
749
+ return `${this.base}${path2}${q}`;
750
+ }
751
+ async matches(q = {}) {
752
+ const params = new URLSearchParams();
753
+ if (q.from) params.set("dateFrom", q.from);
754
+ if (q.to) params.set("dateTo", q.to);
755
+ const data = await httpJson(this.url("/competitions/WC/matches", params));
756
+ return data.matches ?? [];
757
+ }
758
+ async fixtures(q) {
759
+ const raw = await this.matches({ from: q.from, to: q.to });
760
+ let out = raw.map(fixtureFromMatch);
761
+ if (q.teamCode) out = out.filter((f) => f.home.code === q.teamCode || f.away.code === q.teamCode);
762
+ if (q.stage) out = out.filter((f) => f.stage === q.stage);
763
+ return out;
764
+ }
765
+ async liveMatches() {
766
+ const raw = await this.matches();
767
+ return raw.map(fixtureFromMatch).filter((f) => f.status === "live").map((f) => ({ ...f, minute: 0, events: [] }));
768
+ }
769
+ async match(id) {
770
+ const data = await httpJson(this.url(`/matches/${id}`));
771
+ if (!data.id || !data.utcDate || !data.homeTeam || !data.awayTeam || !data.status) {
772
+ throw new WC26Error("NOT_FOUND", `match ${id} not found`);
773
+ }
774
+ const m = {
775
+ id: data.id,
776
+ utcDate: data.utcDate,
777
+ status: data.status,
778
+ homeTeam: data.homeTeam,
779
+ awayTeam: data.awayTeam,
780
+ score: data.score,
781
+ venue: data.venue,
782
+ stage: data.stage,
783
+ group: data.group
784
+ };
785
+ const f = fixtureFromMatch(m);
786
+ const ref = data.referees?.find((r) => /referee/i.test(r.role ?? ""))?.name;
787
+ return {
788
+ ...f,
789
+ minute: 0,
790
+ events: [],
791
+ lineups: { home: [], away: [] },
792
+ stats: {},
793
+ referee: ref
794
+ };
795
+ }
796
+ async standings(group) {
797
+ const data = await httpJson(this.url("/competitions/WC/standings"));
798
+ const out = [];
799
+ for (const s of data.standings ?? []) {
800
+ if (s.type && s.type !== "TOTAL") continue;
801
+ const label = (s.group ?? "").replace(/^GROUP_/i, "").trim().toUpperCase().slice(0, 1);
802
+ if (!label) continue;
803
+ out.push({
804
+ group: label,
805
+ rows: (s.table ?? []).map((r) => ({
806
+ team: teamRef2(r.team),
807
+ p: r.playedGames,
808
+ w: r.won,
809
+ d: r.draw,
810
+ l: r.lost,
811
+ gf: r.goalsFor,
812
+ ga: r.goalsAgainst,
813
+ gd: r.goalDifference,
814
+ pts: r.points
815
+ }))
816
+ });
817
+ }
818
+ out.sort((a, b) => a.group.localeCompare(b.group));
819
+ return group ? out.filter((g) => g.group === group.toUpperCase()) : out;
820
+ }
821
+ async knockoutBracket() {
822
+ const fixtures = await this.fixtures({});
823
+ const stages = ["r16", "qf", "sf", "third", "final"];
824
+ return fixtures.filter((f) => stages.includes(f.stage)).map((f) => ({
825
+ stage: f.stage,
826
+ matchId: f.id,
827
+ home: f.home,
828
+ away: f.away,
829
+ 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
830
+ }));
831
+ }
832
+ async team(code) {
833
+ const data = await httpJson(this.url("/competitions/WC/teams"));
834
+ const list = data.teams ?? [];
835
+ const match = list.find((t) => (t.tla ?? "").toUpperCase() === code.toUpperCase());
836
+ if (!match) throw new WC26Error("NOT_FOUND", `team ${code} not found`);
837
+ let squad = [];
838
+ let coach = match.coach?.name;
839
+ try {
840
+ const t = await httpJson(this.url(`/teams/${match.id}`));
841
+ squad = (t.squad ?? []).map((p) => ({
842
+ name: p.name ?? "?",
843
+ position: p.position ?? "",
844
+ number: p.shirtNumber ?? void 0
845
+ }));
846
+ coach = t.coach?.name ?? coach;
847
+ } catch {
848
+ squad = [];
849
+ }
850
+ const fixtures = await this.fixtures({ teamCode: code });
851
+ return {
852
+ code: (match.tla ?? code).toUpperCase(),
853
+ name: match.name,
854
+ coach,
855
+ squad,
856
+ fixtures
857
+ };
858
+ }
859
+ };
860
+
674
861
  // src/commands/_shared.ts
675
862
  function homeDir() {
676
863
  return process.env.WC26_HOME ?? join3(homedir(), ".wc26");
@@ -691,6 +878,10 @@ async function buildRegistry(opts) {
691
878
  const key = await env.config.apiKey(name) ?? "";
692
879
  if (name === "espn") providers.push(new EspnProvider());
693
880
  else if (name === "thesportsdb") providers.push(new TheSportsDbProvider(key || "3"));
881
+ else if (name === "football-data") {
882
+ const p = new FootballDataProvider();
883
+ if (p.isConfigured()) providers.push(p);
884
+ }
694
885
  }
695
886
  return new ProviderRegistry(providers);
696
887
  }
@@ -793,7 +984,8 @@ var colorStatus = (s) => {
793
984
  return chalk.cyan(s);
794
985
  };
795
986
  var scoreStr2 = (f) => f.score ? `${f.score.home}-${f.score.away}` : "\u2013";
796
- function renderFixturesPretty(fixtures) {
987
+ function renderFixturesPretty(fixtures, emptyMessage = "no matches") {
988
+ if (fixtures.length === 0) return chalk.dim(emptyMessage);
797
989
  const t = new Table({
798
990
  head: ["Kickoff (UTC)", "Stage", "Home", "Away", "Venue", "Score", "Status"],
799
991
  style: { head: ["bold"] }
@@ -916,7 +1108,7 @@ function nextCmd(p) {
916
1108
  else {
917
1109
  if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
918
1110
  `);
919
- process.stdout.write(renderFixturesPretty(out) + "\n");
1111
+ process.stdout.write(renderFixturesPretty(out, "no upcoming matches") + "\n");
920
1112
  }
921
1113
  } catch (e) {
922
1114
  die(e, g);
@@ -1282,7 +1474,7 @@ function cacheCmd(p) {
1282
1474
 
1283
1475
  // src/cli.ts
1284
1476
  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");
1477
+ 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
1478
  fixturesCmd(program);
1287
1479
  nextCmd(program);
1288
1480
  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.0",
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": {