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.
- package/README.md +27 -5
- package/dist/cli.js +196 -4
- 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
|
-
|
|
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
|
|
46
|
+
`wc26` chains providers with automatic failover:
|
|
47
47
|
|
|
48
|
-
1. `
|
|
49
|
-
2. `
|
|
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.
|
|
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.
|
|
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": {
|