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.
- package/README.md +6 -5
- package/dist/cli.js +311 -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. 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
|
|
46
|
+
`wc26` chains providers with automatic failover:
|
|
47
47
|
|
|
48
|
-
1. `
|
|
49
|
-
2. `
|
|
48
|
+
1. `football-data` (api.football-data.org — full 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.
|
|
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
|
|
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": {
|