fifa-wc26 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/cli.js +1289 -0
- package/package.json +78 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/render/live-dashboard.tsx
|
|
22
|
+
var live_dashboard_exports = {};
|
|
23
|
+
__export(live_dashboard_exports, {
|
|
24
|
+
Dashboard: () => Dashboard
|
|
25
|
+
});
|
|
26
|
+
import { Box, Text } from "ink";
|
|
27
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
function scoreStr3(f) {
|
|
29
|
+
return f.score ? `${f.score.home}-${f.score.away}` : "\u2013";
|
|
30
|
+
}
|
|
31
|
+
function Dashboard({ live, upcoming, stale, reason, favoriteOnly }) {
|
|
32
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
33
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
34
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "wc26 live" }),
|
|
35
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
36
|
+
stale && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
37
|
+
"[STALE",
|
|
38
|
+
reason ? " " + reason : "",
|
|
39
|
+
"]"
|
|
40
|
+
] }),
|
|
41
|
+
favoriteOnly && /* @__PURE__ */ jsx(Text, { color: "magenta", children: " [fav-only]" })
|
|
42
|
+
] }),
|
|
43
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: "LIVE" }) }),
|
|
44
|
+
live.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "no live matches" }) : live.map((m) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
45
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
46
|
+
String(m.minute).padStart(3),
|
|
47
|
+
"'"
|
|
48
|
+
] }),
|
|
49
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
50
|
+
" ",
|
|
51
|
+
m.home.code,
|
|
52
|
+
" ",
|
|
53
|
+
scoreStr3(m),
|
|
54
|
+
" ",
|
|
55
|
+
m.away.code,
|
|
56
|
+
" "
|
|
57
|
+
] }),
|
|
58
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
59
|
+
m.stage,
|
|
60
|
+
m.group ? " " + m.group : ""
|
|
61
|
+
] })
|
|
62
|
+
] }, m.id)),
|
|
63
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: "Upcoming" }) }),
|
|
64
|
+
upcoming.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "none" }) : upcoming.map((f) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: f.utcKickoff.replace("T", " ").replace(".000Z", "") }),
|
|
66
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
67
|
+
" ",
|
|
68
|
+
f.home.code,
|
|
69
|
+
" vs ",
|
|
70
|
+
f.away.code,
|
|
71
|
+
" "
|
|
72
|
+
] }),
|
|
73
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
74
|
+
f.stage,
|
|
75
|
+
f.group ? " " + f.group : ""
|
|
76
|
+
] })
|
|
77
|
+
] }, f.id)),
|
|
78
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "q: quit r: refresh t: fav-only ?: help" }) })
|
|
79
|
+
] });
|
|
80
|
+
}
|
|
81
|
+
var init_live_dashboard = __esm({
|
|
82
|
+
"src/render/live-dashboard.tsx"() {
|
|
83
|
+
"use strict";
|
|
84
|
+
init_esm_shims();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/cli.ts
|
|
89
|
+
init_esm_shims();
|
|
90
|
+
import { Command } from "commander";
|
|
91
|
+
|
|
92
|
+
// src/commands/fixtures.ts
|
|
93
|
+
init_esm_shims();
|
|
94
|
+
|
|
95
|
+
// src/commands/_shared.ts
|
|
96
|
+
init_esm_shims();
|
|
97
|
+
import { homedir } from "os";
|
|
98
|
+
import { join as join3 } from "path";
|
|
99
|
+
|
|
100
|
+
// src/config/store.ts
|
|
101
|
+
init_esm_shims();
|
|
102
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
103
|
+
import { join } from "path";
|
|
104
|
+
var DEFAULTS = {
|
|
105
|
+
providers: ["espn", "thesportsdb"],
|
|
106
|
+
apiKeys: {},
|
|
107
|
+
defaults: { watchIntervalSec: 15, output: "pretty" }
|
|
108
|
+
};
|
|
109
|
+
var ENV_KEYS = {
|
|
110
|
+
"thesportsdb": "WC26_THESPORTSDB_KEY"
|
|
111
|
+
};
|
|
112
|
+
var ConfigStore = class {
|
|
113
|
+
constructor(root) {
|
|
114
|
+
this.root = root;
|
|
115
|
+
}
|
|
116
|
+
root;
|
|
117
|
+
file() {
|
|
118
|
+
return join(this.root, "config.json");
|
|
119
|
+
}
|
|
120
|
+
async load() {
|
|
121
|
+
try {
|
|
122
|
+
const raw = await readFile(this.file(), "utf8");
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
return { ...DEFAULTS, ...parsed, defaults: { ...DEFAULTS.defaults, ...parsed.defaults } };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (e.code === "ENOENT") return { ...DEFAULTS };
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async save(cfg) {
|
|
131
|
+
await mkdir(this.root, { recursive: true });
|
|
132
|
+
await writeFile(this.file(), JSON.stringify(cfg, null, 2), "utf8");
|
|
133
|
+
await chmod(this.file(), 384);
|
|
134
|
+
}
|
|
135
|
+
async set(key, value) {
|
|
136
|
+
const cfg = await this.load();
|
|
137
|
+
cfg[key] = value;
|
|
138
|
+
await this.save(cfg);
|
|
139
|
+
}
|
|
140
|
+
async setApiKey(provider, key) {
|
|
141
|
+
const cfg = await this.load();
|
|
142
|
+
cfg.apiKeys[provider] = key;
|
|
143
|
+
await this.save(cfg);
|
|
144
|
+
}
|
|
145
|
+
async apiKey(provider) {
|
|
146
|
+
const env2 = ENV_KEYS[provider];
|
|
147
|
+
if (env2 && process.env[env2]) return process.env[env2];
|
|
148
|
+
const cfg = await this.load();
|
|
149
|
+
return cfg.apiKeys[provider];
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/cache/json-cache.ts
|
|
154
|
+
init_esm_shims();
|
|
155
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rm, readdir } from "fs/promises";
|
|
156
|
+
import { dirname, join as join2 } from "path";
|
|
157
|
+
function sanitize(key) {
|
|
158
|
+
return key.split("/").map((seg) => seg.replace(/[^A-Za-z0-9_-]/g, "_")).filter(Boolean).join("/");
|
|
159
|
+
}
|
|
160
|
+
var JsonCache = class {
|
|
161
|
+
constructor(root) {
|
|
162
|
+
this.root = root;
|
|
163
|
+
}
|
|
164
|
+
root;
|
|
165
|
+
pathFor(key) {
|
|
166
|
+
return join2(this.root, `${sanitize(key)}.json`);
|
|
167
|
+
}
|
|
168
|
+
async read(key, opts = {}) {
|
|
169
|
+
try {
|
|
170
|
+
const raw = await readFile2(this.pathFor(key), "utf8");
|
|
171
|
+
const env2 = JSON.parse(raw);
|
|
172
|
+
const age = (Date.now() - new Date(env2.fetchedAt).getTime()) / 1e3;
|
|
173
|
+
const stale = age >= env2.ttlSec;
|
|
174
|
+
if (stale && !opts.allowStale) return null;
|
|
175
|
+
return { data: env2.data, stale, fetchedAt: env2.fetchedAt, provider: env2.provider };
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e.code === "ENOENT") return null;
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async write(key, data, ttlSec, provider) {
|
|
182
|
+
const file = this.pathFor(key);
|
|
183
|
+
await mkdir2(dirname(file), { recursive: true });
|
|
184
|
+
const env2 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), ttlSec, provider, data };
|
|
185
|
+
await writeFile2(file, JSON.stringify(env2), "utf8");
|
|
186
|
+
}
|
|
187
|
+
async clear(resource) {
|
|
188
|
+
if (!resource) {
|
|
189
|
+
await rm(this.root, { recursive: true, force: true });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const target = join2(this.root, sanitize(resource));
|
|
193
|
+
try {
|
|
194
|
+
const entries = await readdir(target);
|
|
195
|
+
await Promise.all(entries.map((f) => rm(join2(target, f), { force: true })));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (e.code !== "ENOENT") throw e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/providers/registry.ts
|
|
203
|
+
init_esm_shims();
|
|
204
|
+
|
|
205
|
+
// src/errors.ts
|
|
206
|
+
init_esm_shims();
|
|
207
|
+
var WC26Error = class extends Error {
|
|
208
|
+
constructor(code, message, cause) {
|
|
209
|
+
super(message);
|
|
210
|
+
this.code = code;
|
|
211
|
+
this.cause = cause;
|
|
212
|
+
this.name = "WC26Error";
|
|
213
|
+
}
|
|
214
|
+
code;
|
|
215
|
+
cause;
|
|
216
|
+
};
|
|
217
|
+
var EXIT = {
|
|
218
|
+
NO_PROVIDER_CONFIGURED: 3,
|
|
219
|
+
PROVIDER_UNREACHABLE: 4,
|
|
220
|
+
CACHE_MISS_OFFLINE: 4,
|
|
221
|
+
NOT_FOUND: 5,
|
|
222
|
+
RATE_LIMIT: 1,
|
|
223
|
+
SCHEMA_MISMATCH: 1,
|
|
224
|
+
CONFIG_INVALID: 1
|
|
225
|
+
};
|
|
226
|
+
function exitCodeFor(code) {
|
|
227
|
+
return EXIT[code];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/providers/registry.ts
|
|
231
|
+
var FAIL_THRESHOLD = 3;
|
|
232
|
+
var OPEN_MS = 5 * 60 * 1e3;
|
|
233
|
+
var ProviderRegistry = class {
|
|
234
|
+
constructor(providers) {
|
|
235
|
+
this.providers = providers;
|
|
236
|
+
}
|
|
237
|
+
providers;
|
|
238
|
+
breakers = /* @__PURE__ */ new Map();
|
|
239
|
+
breaker(name) {
|
|
240
|
+
let b = this.breakers.get(name);
|
|
241
|
+
if (!b) {
|
|
242
|
+
b = { failures: 0, openedAt: null };
|
|
243
|
+
this.breakers.set(name, b);
|
|
244
|
+
}
|
|
245
|
+
return b;
|
|
246
|
+
}
|
|
247
|
+
isOpen(name) {
|
|
248
|
+
const b = this.breaker(name);
|
|
249
|
+
if (b.openedAt == null) return false;
|
|
250
|
+
if (Date.now() - b.openedAt < OPEN_MS) return true;
|
|
251
|
+
b.openedAt = null;
|
|
252
|
+
b.failures = 0;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
recordFail(name) {
|
|
256
|
+
const b = this.breaker(name);
|
|
257
|
+
b.failures++;
|
|
258
|
+
if (b.failures >= FAIL_THRESHOLD) b.openedAt = Date.now();
|
|
259
|
+
}
|
|
260
|
+
recordOk(name) {
|
|
261
|
+
const b = this.breaker(name);
|
|
262
|
+
b.failures = 0;
|
|
263
|
+
b.openedAt = null;
|
|
264
|
+
}
|
|
265
|
+
async call(fn) {
|
|
266
|
+
const candidates = this.providers.filter((p) => p.isConfigured() && !this.isOpen(p.name));
|
|
267
|
+
if (candidates.length === 0) {
|
|
268
|
+
if (this.providers.length === 0 || !this.providers.some((p) => p.isConfigured())) {
|
|
269
|
+
throw new WC26Error("NO_PROVIDER_CONFIGURED", "no provider configured. defaults are keyless (espn, thesportsdb).");
|
|
270
|
+
}
|
|
271
|
+
throw new WC26Error("PROVIDER_UNREACHABLE", "all providers temporarily unavailable");
|
|
272
|
+
}
|
|
273
|
+
let lastErr;
|
|
274
|
+
for (const p of candidates) {
|
|
275
|
+
try {
|
|
276
|
+
const out = await fn(p);
|
|
277
|
+
this.recordOk(p.name);
|
|
278
|
+
return out;
|
|
279
|
+
} catch (e) {
|
|
280
|
+
if (e instanceof WC26Error && e.code === "NOT_FOUND") throw e;
|
|
281
|
+
this.recordFail(p.name);
|
|
282
|
+
lastErr = e instanceof WC26Error ? e : new WC26Error("PROVIDER_UNREACHABLE", String(e));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw new WC26Error("PROVIDER_UNREACHABLE", lastErr?.message ?? "all providers failed");
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/providers/espn.ts
|
|
290
|
+
init_esm_shims();
|
|
291
|
+
|
|
292
|
+
// src/http/client.ts
|
|
293
|
+
init_esm_shims();
|
|
294
|
+
import { fetch } from "undici";
|
|
295
|
+
var TRANSIENT = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
296
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
297
|
+
async function httpJson(url, opts = {}) {
|
|
298
|
+
const retries = opts.retries ?? 3;
|
|
299
|
+
const base2 = opts.baseDelayMs ?? 1e3;
|
|
300
|
+
let attempt = 0;
|
|
301
|
+
let lastErr;
|
|
302
|
+
while (attempt <= retries) {
|
|
303
|
+
try {
|
|
304
|
+
const res = await fetch(url, {
|
|
305
|
+
headers: opts.headers,
|
|
306
|
+
signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : void 0
|
|
307
|
+
});
|
|
308
|
+
if (res.ok) return await res.json();
|
|
309
|
+
if (res.status === 404) {
|
|
310
|
+
throw new WC26Error("NOT_FOUND", `not found: ${url}`);
|
|
311
|
+
}
|
|
312
|
+
if (!TRANSIENT.has(res.status)) {
|
|
313
|
+
throw new WC26Error("PROVIDER_UNREACHABLE", `http ${res.status}: ${url}`);
|
|
314
|
+
}
|
|
315
|
+
lastErr = res.status === 429 ? new WC26Error("RATE_LIMIT", `rate limited: ${url}`) : new WC26Error("PROVIDER_UNREACHABLE", `http ${res.status}: ${url}`);
|
|
316
|
+
if (attempt === retries) throw lastErr;
|
|
317
|
+
const retryAfter = res.headers.get("retry-after");
|
|
318
|
+
const delay = retryAfter ? Number(retryAfter) * 1e3 : base2 * 2 ** attempt;
|
|
319
|
+
await sleep(Math.max(0, delay));
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (e instanceof WC26Error && (e.code === "NOT_FOUND" || attempt === retries)) throw e;
|
|
322
|
+
if (!(e instanceof WC26Error)) {
|
|
323
|
+
lastErr = new WC26Error("PROVIDER_UNREACHABLE", `network error: ${url}`, e);
|
|
324
|
+
if (attempt === retries) throw lastErr;
|
|
325
|
+
await sleep(base2 * 2 ** attempt);
|
|
326
|
+
} else {
|
|
327
|
+
lastErr = e;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
attempt++;
|
|
331
|
+
}
|
|
332
|
+
throw lastErr ?? new WC26Error("PROVIDER_UNREACHABLE", `exhausted retries: ${url}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/providers/espn.ts
|
|
336
|
+
var BASE = "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.worldcup";
|
|
337
|
+
var STANDINGS_BASE = "https://site.api.espn.com/apis/v2/sports/soccer/fifa.worldcup";
|
|
338
|
+
var SEASON = 2026;
|
|
339
|
+
function mapStage(headline, typeName) {
|
|
340
|
+
const text = `${headline ?? ""} ${typeName ?? ""}`.toLowerCase();
|
|
341
|
+
if (/third[- ]place|3rd[- ]place/.test(text)) return { stage: "third" };
|
|
342
|
+
if (/\bfinal\b/.test(text) && !/semi|quarter|third|3rd/.test(text)) return { stage: "final" };
|
|
343
|
+
if (/semi/.test(text)) return { stage: "sf" };
|
|
344
|
+
if (/quarter|qf\b/.test(text)) return { stage: "qf" };
|
|
345
|
+
if (/round of 16|r16|last 16/.test(text)) return { stage: "r16" };
|
|
346
|
+
const g = text.match(/group\s+([a-l])\b/i);
|
|
347
|
+
return { stage: "group", group: g?.[1]?.toUpperCase() };
|
|
348
|
+
}
|
|
349
|
+
function mapStatus(name, state) {
|
|
350
|
+
if (name === "STATUS_POSTPONED") return "postponed";
|
|
351
|
+
if (name === "STATUS_CANCELED" || name === "STATUS_FORFEIT") return "cancelled";
|
|
352
|
+
if (state === "in") return "live";
|
|
353
|
+
if (state === "post") return "finished";
|
|
354
|
+
return "scheduled";
|
|
355
|
+
}
|
|
356
|
+
function clockMinute(displayClock, period) {
|
|
357
|
+
if (!displayClock) return 0;
|
|
358
|
+
const m = displayClock.match(/^(\d+)/);
|
|
359
|
+
const base2 = m ? Number(m[1]) : 0;
|
|
360
|
+
if (period === 2) return Math.max(base2, 45);
|
|
361
|
+
return base2;
|
|
362
|
+
}
|
|
363
|
+
function teamRef(c) {
|
|
364
|
+
const name = c.team.displayName ?? c.team.name ?? c.team.abbreviation ?? "?";
|
|
365
|
+
const code = (c.team.abbreviation ?? name.slice(0, 3)).toUpperCase().slice(0, 3);
|
|
366
|
+
return { code, name };
|
|
367
|
+
}
|
|
368
|
+
function fixtureFromEvent(ev) {
|
|
369
|
+
const comp = ev.competitions[0];
|
|
370
|
+
if (!comp) throw new WC26Error("PROVIDER_UNREACHABLE", `event ${ev.id} has no competition`);
|
|
371
|
+
const home = comp.competitors.find((c) => c.homeAway === "home") ?? comp.competitors[0];
|
|
372
|
+
const away = comp.competitors.find((c) => c.homeAway === "away") ?? comp.competitors[1];
|
|
373
|
+
if (!home || !away) throw new WC26Error("PROVIDER_UNREACHABLE", `event ${ev.id} missing competitors`);
|
|
374
|
+
const status = mapStatus(comp.status?.type?.name ?? ev.status?.type?.name, comp.status?.type?.state ?? ev.status?.type?.state);
|
|
375
|
+
const note = comp.notes?.[0]?.headline;
|
|
376
|
+
const { stage, group } = mapStage(note, comp.status?.type?.description);
|
|
377
|
+
const hs = Number(home.score);
|
|
378
|
+
const as = Number(away.score);
|
|
379
|
+
const score = Number.isFinite(hs) && Number.isFinite(as) && (status === "live" || status === "finished") ? { home: hs, away: as } : void 0;
|
|
380
|
+
return {
|
|
381
|
+
id: ev.id,
|
|
382
|
+
utcKickoff: new Date(ev.date).toISOString(),
|
|
383
|
+
stage,
|
|
384
|
+
group,
|
|
385
|
+
home: teamRef(home),
|
|
386
|
+
away: teamRef(away),
|
|
387
|
+
venue: comp.venue?.fullName ?? "",
|
|
388
|
+
status,
|
|
389
|
+
score
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function eventToLive(ev) {
|
|
393
|
+
const base2 = fixtureFromEvent(ev);
|
|
394
|
+
const comp = ev.competitions[0];
|
|
395
|
+
const minute = clockMinute(comp.status?.displayClock, comp.status?.period);
|
|
396
|
+
const homeId = comp.competitors.find((c) => c.homeAway === "home")?.team.id;
|
|
397
|
+
const events = (comp.details ?? []).map((d) => {
|
|
398
|
+
const min = Number((d.clock?.displayValue ?? "0").match(/\d+/)?.[0] ?? 0);
|
|
399
|
+
let type = "var";
|
|
400
|
+
if (d.scoringPlay) type = d.ownGoal ? "own-goal" : d.penaltyKick ? "penalty" : "goal";
|
|
401
|
+
else if (d.redCard) type = "red";
|
|
402
|
+
else if (d.yellowCard) type = "yellow";
|
|
403
|
+
else if ((d.type?.text ?? "").toLowerCase().includes("substitution")) type = "sub";
|
|
404
|
+
return {
|
|
405
|
+
minute: min,
|
|
406
|
+
type,
|
|
407
|
+
team: d.team?.id === homeId ? "home" : "away",
|
|
408
|
+
player: d.athletesInvolved?.[0]?.displayName,
|
|
409
|
+
detail: d.text ?? d.type?.text
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
return { ...base2, minute, events };
|
|
413
|
+
}
|
|
414
|
+
function fmtDate(d) {
|
|
415
|
+
return d.replace(/-/g, "");
|
|
416
|
+
}
|
|
417
|
+
var EspnProvider = class {
|
|
418
|
+
name = "espn";
|
|
419
|
+
isConfigured() {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
async scoreboard(dates) {
|
|
423
|
+
const qs = dates ? `?dates=${dates}` : `?dates=${SEASON}`;
|
|
424
|
+
const data = await httpJson(`${BASE}/scoreboard${qs}`);
|
|
425
|
+
return data.events ?? [];
|
|
426
|
+
}
|
|
427
|
+
async fixtures(q) {
|
|
428
|
+
let dates;
|
|
429
|
+
if (q.from && q.to) dates = `${fmtDate(q.from)}-${fmtDate(q.to)}`;
|
|
430
|
+
else if (q.from) dates = fmtDate(q.from);
|
|
431
|
+
const events = await this.scoreboard(dates);
|
|
432
|
+
let out = events.map(fixtureFromEvent);
|
|
433
|
+
if (q.teamCode) out = out.filter((f) => f.home.code === q.teamCode || f.away.code === q.teamCode);
|
|
434
|
+
if (q.stage) out = out.filter((f) => f.stage === q.stage);
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
async liveMatches() {
|
|
438
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
439
|
+
const events = await this.scoreboard(today);
|
|
440
|
+
return events.filter((e) => (e.competitions[0]?.status?.type?.state ?? e.status?.type?.state) === "in").map(eventToLive);
|
|
441
|
+
}
|
|
442
|
+
async match(id) {
|
|
443
|
+
const data = await httpJson(
|
|
444
|
+
`${BASE}/summary?event=${id}`
|
|
445
|
+
);
|
|
446
|
+
const comp = data.header?.competitions?.[0];
|
|
447
|
+
if (!comp || !data.header) throw new WC26Error("NOT_FOUND", `match ${id} not found`);
|
|
448
|
+
const ev = { id: data.header.id ?? id, date: comp.date, competitions: [comp] };
|
|
449
|
+
const live = eventToLive(ev);
|
|
450
|
+
const homeRoster = data.rosters?.find((r) => r.homeAway === "home")?.roster ?? [];
|
|
451
|
+
const awayRoster = data.rosters?.find((r) => r.homeAway === "away")?.roster ?? [];
|
|
452
|
+
const mapPlayer = (r) => ({
|
|
453
|
+
name: r.athlete?.displayName ?? "?",
|
|
454
|
+
position: r.athlete?.position?.abbreviation ?? "",
|
|
455
|
+
number: r.athlete?.jersey ? Number(r.athlete.jersey) : void 0
|
|
456
|
+
});
|
|
457
|
+
const ref = data.gameInfo?.officials?.find((o) => /referee/i.test(o.position?.name ?? ""))?.displayName;
|
|
458
|
+
return {
|
|
459
|
+
...live,
|
|
460
|
+
lineups: { home: homeRoster.map(mapPlayer), away: awayRoster.map(mapPlayer) },
|
|
461
|
+
stats: {},
|
|
462
|
+
referee: ref
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async standings(group) {
|
|
466
|
+
const data = await httpJson(`${STANDINGS_BASE}/standings?season=${SEASON}`);
|
|
467
|
+
const stat = (entry, names) => {
|
|
468
|
+
for (const n of names) {
|
|
469
|
+
const s = entry.stats.find((x) => x.name === n);
|
|
470
|
+
if (s && typeof s.value === "number") return s.value;
|
|
471
|
+
}
|
|
472
|
+
return 0;
|
|
473
|
+
};
|
|
474
|
+
const groups = [];
|
|
475
|
+
for (const child of data.children ?? []) {
|
|
476
|
+
const label = (child.abbreviation ?? child.name ?? "").match(/[A-Z]$/)?.[0] ?? (child.name ?? "").replace(/^Group\s+/i, "").trim();
|
|
477
|
+
if (!label) continue;
|
|
478
|
+
groups.push({
|
|
479
|
+
group: label.toUpperCase(),
|
|
480
|
+
rows: (child.standings?.entries ?? []).map((e) => ({
|
|
481
|
+
team: { code: (e.team.abbreviation ?? e.team.displayName.slice(0, 3)).toUpperCase(), name: e.team.displayName },
|
|
482
|
+
p: stat(e, ["gamesPlayed"]),
|
|
483
|
+
w: stat(e, ["wins"]),
|
|
484
|
+
d: stat(e, ["ties", "draws"]),
|
|
485
|
+
l: stat(e, ["losses"]),
|
|
486
|
+
gf: stat(e, ["pointsFor", "goalsFor"]),
|
|
487
|
+
ga: stat(e, ["pointsAgainst", "goalsAgainst"]),
|
|
488
|
+
gd: stat(e, ["pointDifferential", "goalDifferential", "differential"]),
|
|
489
|
+
pts: stat(e, ["points"])
|
|
490
|
+
}))
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
groups.sort((a, b) => a.group.localeCompare(b.group));
|
|
494
|
+
return group ? groups.filter((g) => g.group === group.toUpperCase()) : groups;
|
|
495
|
+
}
|
|
496
|
+
async knockoutBracket() {
|
|
497
|
+
const fixtures = await this.fixtures({});
|
|
498
|
+
const stages = ["r16", "qf", "sf", "third", "final"];
|
|
499
|
+
return fixtures.filter((f) => stages.includes(f.stage)).map((f) => ({
|
|
500
|
+
stage: f.stage,
|
|
501
|
+
matchId: f.id,
|
|
502
|
+
home: f.home,
|
|
503
|
+
away: f.away,
|
|
504
|
+
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
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
async team(code) {
|
|
508
|
+
const data = await httpJson(`${BASE}/teams`);
|
|
509
|
+
const teams = data.sports?.[0]?.leagues?.[0]?.teams ?? [];
|
|
510
|
+
const match = teams.find((t2) => (t2.team.abbreviation ?? "").toUpperCase() === code.toUpperCase());
|
|
511
|
+
if (!match) throw new WC26Error("NOT_FOUND", `team ${code} not found`);
|
|
512
|
+
const t = match.team;
|
|
513
|
+
let squad = [];
|
|
514
|
+
try {
|
|
515
|
+
const roster = await httpJson(`${BASE}/teams/${t.id}/roster`);
|
|
516
|
+
squad = (roster.athletes ?? []).map((a) => ({
|
|
517
|
+
name: a.displayName ?? "?",
|
|
518
|
+
position: a.position?.abbreviation ?? "",
|
|
519
|
+
number: a.jersey ? Number(a.jersey) : void 0
|
|
520
|
+
}));
|
|
521
|
+
} catch {
|
|
522
|
+
squad = [];
|
|
523
|
+
}
|
|
524
|
+
const fixtures = await this.fixtures({ teamCode: code });
|
|
525
|
+
return {
|
|
526
|
+
code: (t.abbreviation ?? code).toUpperCase(),
|
|
527
|
+
name: t.displayName,
|
|
528
|
+
squad,
|
|
529
|
+
fixtures
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/providers/thesportsdb.ts
|
|
535
|
+
init_esm_shims();
|
|
536
|
+
var LEAGUE_ID = "4429";
|
|
537
|
+
var SEASON2 = "2026";
|
|
538
|
+
function base(key) {
|
|
539
|
+
return `https://www.thesportsdb.com/api/v1/json/${encodeURIComponent(key || "3")}`;
|
|
540
|
+
}
|
|
541
|
+
function mapStage2(round) {
|
|
542
|
+
const r = (round ?? "").toLowerCase();
|
|
543
|
+
if (/final/.test(r) && !/semi|quarter|third|3rd/.test(r)) return { stage: "final" };
|
|
544
|
+
if (/third|3rd/.test(r)) return { stage: "third" };
|
|
545
|
+
if (/semi/.test(r)) return { stage: "sf" };
|
|
546
|
+
if (/quarter/.test(r)) return { stage: "qf" };
|
|
547
|
+
if (/round of 16|r16|last 16/.test(r)) return { stage: "r16" };
|
|
548
|
+
const g = r.match(/group\s+([a-l])/);
|
|
549
|
+
return { stage: "group", group: g?.[1]?.toUpperCase() };
|
|
550
|
+
}
|
|
551
|
+
function teamCode(name, fallback) {
|
|
552
|
+
const raw = (fallback ?? name ?? "?").toUpperCase().replace(/[^A-Z]/g, "");
|
|
553
|
+
return raw.slice(0, 3) || "???";
|
|
554
|
+
}
|
|
555
|
+
function mapStatus2(s) {
|
|
556
|
+
const status = (s.strStatus ?? "").toLowerCase();
|
|
557
|
+
if (s.strPostponed === "yes" || status.includes("postp")) return "postponed";
|
|
558
|
+
if (status.includes("cancel")) return "cancelled";
|
|
559
|
+
if (/ft|finished|full[- ]time|match finished/.test(status)) return "finished";
|
|
560
|
+
if (/1h|2h|ht|live|in progress|playing/.test(status)) return "live";
|
|
561
|
+
if (s.intHomeScore != null && s.intAwayScore != null) return "finished";
|
|
562
|
+
return "scheduled";
|
|
563
|
+
}
|
|
564
|
+
function fixtureFromTSDB(e) {
|
|
565
|
+
const ts = e.strTimestamp ? new Date(e.strTimestamp) : /* @__PURE__ */ new Date(`${e.dateEvent}T${e.strTime ?? "00:00:00"}Z`);
|
|
566
|
+
const status = mapStatus2(e);
|
|
567
|
+
const hs = e.intHomeScore != null ? Number(e.intHomeScore) : NaN;
|
|
568
|
+
const as = e.intAwayScore != null ? Number(e.intAwayScore) : NaN;
|
|
569
|
+
const score = Number.isFinite(hs) && Number.isFinite(as) ? { home: hs, away: as } : void 0;
|
|
570
|
+
const round = e.strRound ?? e.strGroup ?? "";
|
|
571
|
+
const { stage, group: gFromRound } = mapStage2(round);
|
|
572
|
+
const group = gFromRound ?? (e.strGroup ? e.strGroup.replace(/^Group\s+/i, "").trim().toUpperCase().slice(0, 1) : void 0);
|
|
573
|
+
return {
|
|
574
|
+
id: e.idEvent,
|
|
575
|
+
utcKickoff: ts.toISOString(),
|
|
576
|
+
stage,
|
|
577
|
+
group: group || void 0,
|
|
578
|
+
home: { code: teamCode(e.strHomeTeam, e.strHomeTeamShort), name: e.strHomeTeam ?? "?" },
|
|
579
|
+
away: { code: teamCode(e.strAwayTeam, e.strAwayTeamShort), name: e.strAwayTeam ?? "?" },
|
|
580
|
+
venue: e.strVenue ?? "",
|
|
581
|
+
status,
|
|
582
|
+
score
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
var TheSportsDbProvider = class {
|
|
586
|
+
constructor(apiKey = "3") {
|
|
587
|
+
this.apiKey = apiKey;
|
|
588
|
+
}
|
|
589
|
+
apiKey;
|
|
590
|
+
name = "thesportsdb";
|
|
591
|
+
isConfigured() {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
async fixtures(q) {
|
|
595
|
+
const data = await httpJson(
|
|
596
|
+
`${base(this.apiKey)}/eventsseason.php?id=${LEAGUE_ID}&s=${SEASON2}`
|
|
597
|
+
);
|
|
598
|
+
let out = (data.events ?? []).map(fixtureFromTSDB);
|
|
599
|
+
if (q.from) out = out.filter((f) => f.utcKickoff.slice(0, 10) >= q.from);
|
|
600
|
+
if (q.to) out = out.filter((f) => f.utcKickoff.slice(0, 10) <= q.to);
|
|
601
|
+
if (q.teamCode) out = out.filter((f) => f.home.code === q.teamCode || f.away.code === q.teamCode);
|
|
602
|
+
if (q.stage) out = out.filter((f) => f.stage === q.stage);
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
async liveMatches() {
|
|
606
|
+
const data = await httpJson(
|
|
607
|
+
`${base(this.apiKey)}/livescore.php?l=Soccer`
|
|
608
|
+
).catch(() => ({ events: [] }));
|
|
609
|
+
return (data.events ?? []).filter((e) => e.idLeague === LEAGUE_ID || (e.strEvent ?? "").includes("World Cup")).map((e) => ({ ...fixtureFromTSDB(e), minute: 0, events: [] }));
|
|
610
|
+
}
|
|
611
|
+
async match(id) {
|
|
612
|
+
const data = await httpJson(`${base(this.apiKey)}/lookupevent.php?id=${id}`);
|
|
613
|
+
const e = data.events?.[0];
|
|
614
|
+
if (!e) throw new WC26Error("NOT_FOUND", `match ${id} not found`);
|
|
615
|
+
const f = fixtureFromTSDB(e);
|
|
616
|
+
return { ...f, minute: 0, events: [], lineups: { home: [], away: [] }, stats: {} };
|
|
617
|
+
}
|
|
618
|
+
async standings(group) {
|
|
619
|
+
const data = await httpJson(`${base(this.apiKey)}/lookuptable.php?l=${LEAGUE_ID}&s=${SEASON2}`);
|
|
620
|
+
const groups = /* @__PURE__ */ new Map();
|
|
621
|
+
for (const row of data.table ?? []) {
|
|
622
|
+
const label = (row.strGroup ?? "").replace(/^Group\s+/i, "").trim().toUpperCase().slice(0, 1) || "?";
|
|
623
|
+
if (!groups.has(label)) groups.set(label, { group: label, rows: [] });
|
|
624
|
+
groups.get(label).rows.push({
|
|
625
|
+
team: { code: teamCode(row.strTeam, row.strTeamShort), name: row.strTeam },
|
|
626
|
+
p: Number(row.intPlayed ?? 0),
|
|
627
|
+
w: Number(row.intWin ?? 0),
|
|
628
|
+
d: Number(row.intDraw ?? 0),
|
|
629
|
+
l: Number(row.intLoss ?? 0),
|
|
630
|
+
gf: Number(row.intGoalsFor ?? 0),
|
|
631
|
+
ga: Number(row.intGoalsAgainst ?? 0),
|
|
632
|
+
gd: Number(row.intGoalDifference ?? 0),
|
|
633
|
+
pts: Number(row.intPoints ?? 0)
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const out = [...groups.values()].sort((a, b) => a.group.localeCompare(b.group));
|
|
637
|
+
return group ? out.filter((g) => g.group === group.toUpperCase()) : out;
|
|
638
|
+
}
|
|
639
|
+
async knockoutBracket() {
|
|
640
|
+
const fixtures = await this.fixtures({});
|
|
641
|
+
const stages = ["r16", "qf", "sf", "third", "final"];
|
|
642
|
+
return fixtures.filter((f) => stages.includes(f.stage)).map((f) => ({
|
|
643
|
+
stage: f.stage,
|
|
644
|
+
matchId: f.id,
|
|
645
|
+
home: f.home,
|
|
646
|
+
away: f.away,
|
|
647
|
+
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
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
async team(code) {
|
|
651
|
+
const data = await httpJson(`${base(this.apiKey)}/search_all_teams.php?l=FIFA%20World%20Cup`).catch(() => ({ teams: [] }));
|
|
652
|
+
const list = data.teams ?? [];
|
|
653
|
+
const t = list.find((x) => teamCode(x.strTeam, x.strTeamShort) === code.toUpperCase());
|
|
654
|
+
if (!t) throw new WC26Error("NOT_FOUND", `team ${code} not found`);
|
|
655
|
+
const fixtures = await this.fixtures({ teamCode: code });
|
|
656
|
+
return {
|
|
657
|
+
code: teamCode(t.strTeam, t.strTeamShort),
|
|
658
|
+
name: t.strTeam,
|
|
659
|
+
squad: [],
|
|
660
|
+
fixtures
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/commands/_shared.ts
|
|
666
|
+
function homeDir() {
|
|
667
|
+
return process.env.WC26_HOME ?? join3(homedir(), ".wc26");
|
|
668
|
+
}
|
|
669
|
+
var env = {
|
|
670
|
+
get config() {
|
|
671
|
+
return new ConfigStore(homeDir());
|
|
672
|
+
},
|
|
673
|
+
get cache() {
|
|
674
|
+
return new JsonCache(join3(homeDir(), "cache"));
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
async function buildRegistry(opts) {
|
|
678
|
+
const cfg = await env.config.load();
|
|
679
|
+
const names = opts.provider ? [opts.provider] : cfg.providers;
|
|
680
|
+
const providers = [];
|
|
681
|
+
for (const name of names) {
|
|
682
|
+
const key = await env.config.apiKey(name) ?? "";
|
|
683
|
+
if (name === "espn") providers.push(new EspnProvider());
|
|
684
|
+
else if (name === "thesportsdb") providers.push(new TheSportsDbProvider(key || "3"));
|
|
685
|
+
}
|
|
686
|
+
return new ProviderRegistry(providers);
|
|
687
|
+
}
|
|
688
|
+
async function runCached(key, ttlSec, fetcher, opts) {
|
|
689
|
+
if (!opts.noCache) {
|
|
690
|
+
const hit = await env.cache.read(key, { allowStale: false });
|
|
691
|
+
if (hit) return { data: hit.data, stale: false };
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const { data, provider } = await fetcher();
|
|
695
|
+
await env.cache.write(key, data, ttlSec, provider);
|
|
696
|
+
return { data, stale: false };
|
|
697
|
+
} catch (e) {
|
|
698
|
+
const stale = await env.cache.read(key, { allowStale: true });
|
|
699
|
+
if (stale) {
|
|
700
|
+
const reason = e instanceof WC26Error ? e.code.toLowerCase() : "unreachable";
|
|
701
|
+
return { data: stale.data, stale: true, reason };
|
|
702
|
+
}
|
|
703
|
+
throw e;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function die(e, opts) {
|
|
707
|
+
if (e instanceof WC26Error) {
|
|
708
|
+
if (opts.json) {
|
|
709
|
+
process.stderr.write(JSON.stringify({ error: { code: e.code, message: e.message } }) + "\n");
|
|
710
|
+
} else {
|
|
711
|
+
process.stderr.write(`\u2717 ${e.message}
|
|
712
|
+
`);
|
|
713
|
+
}
|
|
714
|
+
if (opts.verbose && e.cause) process.stderr.write(String(e.cause) + "\n");
|
|
715
|
+
process.exit(exitCodeFor(e.code));
|
|
716
|
+
}
|
|
717
|
+
process.stderr.write(String(e) + "\n");
|
|
718
|
+
if (opts.verbose && e instanceof Error && e.stack) process.stderr.write(e.stack + "\n");
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/render/json.ts
|
|
723
|
+
init_esm_shims();
|
|
724
|
+
function renderJson(data, opts = {}) {
|
|
725
|
+
return JSON.stringify({ stale: !!opts.stale, ...opts.reason ? { reason: opts.reason } : {}, data });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/render/plain.ts
|
|
729
|
+
init_esm_shims();
|
|
730
|
+
var scoreStr = (f) => f.score ? `${f.score.home}-${f.score.away}` : "-";
|
|
731
|
+
function renderFixturesPlain(items) {
|
|
732
|
+
const lines = ["id kickoff stage group home away venue status score"];
|
|
733
|
+
for (const f of items) {
|
|
734
|
+
lines.push([f.id, f.utcKickoff, f.stage, f.group ?? "-", f.home.code, f.away.code, f.venue, f.status, scoreStr(f)].join(" "));
|
|
735
|
+
}
|
|
736
|
+
return lines.join("\n") + "\n";
|
|
737
|
+
}
|
|
738
|
+
function renderLivePlain(items) {
|
|
739
|
+
const lines = ["id minute home away score status"];
|
|
740
|
+
for (const m of items) {
|
|
741
|
+
lines.push([m.id, m.minute, m.home.code, m.away.code, scoreStr(m), m.status].join(" "));
|
|
742
|
+
}
|
|
743
|
+
return lines.join("\n") + "\n";
|
|
744
|
+
}
|
|
745
|
+
function renderStandingsPlain(groups) {
|
|
746
|
+
const lines = ["group team p w d l gf ga gd pts"];
|
|
747
|
+
for (const g of groups) {
|
|
748
|
+
for (const r of g.rows) {
|
|
749
|
+
lines.push([g.group, r.team.code, r.p, r.w, r.d, r.l, r.gf, r.ga, r.gd, r.pts].join(" "));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return lines.join("\n") + "\n";
|
|
753
|
+
}
|
|
754
|
+
function renderBracketPlain(nodes) {
|
|
755
|
+
const lines = ["stage home away winner"];
|
|
756
|
+
for (const n of nodes) {
|
|
757
|
+
lines.push([n.stage, n.home?.code ?? "?", n.away?.code ?? "?", n.winner?.code ?? "-"].join(" "));
|
|
758
|
+
}
|
|
759
|
+
return lines.join("\n") + "\n";
|
|
760
|
+
}
|
|
761
|
+
function renderTeamPlain(t) {
|
|
762
|
+
return [
|
|
763
|
+
`code name group`,
|
|
764
|
+
`${t.code} ${t.name} ${t.group ?? "-"}`
|
|
765
|
+
].join("\n") + "\n";
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/render/pretty.ts
|
|
769
|
+
init_esm_shims();
|
|
770
|
+
import Table from "cli-table3";
|
|
771
|
+
import chalk from "chalk";
|
|
772
|
+
var stageLabel = (s) => ({
|
|
773
|
+
group: "Group",
|
|
774
|
+
r16: "R16",
|
|
775
|
+
qf: "QF",
|
|
776
|
+
sf: "SF",
|
|
777
|
+
third: "3rd",
|
|
778
|
+
final: "Final"
|
|
779
|
+
})[s] ?? s;
|
|
780
|
+
var colorStatus = (s) => {
|
|
781
|
+
if (s === "live") return chalk.green(s);
|
|
782
|
+
if (s === "finished") return chalk.gray(s);
|
|
783
|
+
if (s === "cancelled" || s === "postponed") return chalk.red(s);
|
|
784
|
+
return chalk.cyan(s);
|
|
785
|
+
};
|
|
786
|
+
var scoreStr2 = (f) => f.score ? `${f.score.home}-${f.score.away}` : "\u2013";
|
|
787
|
+
function renderFixturesPretty(fixtures) {
|
|
788
|
+
const t = new Table({
|
|
789
|
+
head: ["Kickoff (UTC)", "Stage", "Home", "Away", "Venue", "Score", "Status"],
|
|
790
|
+
style: { head: ["bold"] }
|
|
791
|
+
});
|
|
792
|
+
for (const f of fixtures) {
|
|
793
|
+
t.push([
|
|
794
|
+
f.utcKickoff.replace("T", " ").replace(".000Z", ""),
|
|
795
|
+
`${stageLabel(f.stage)}${f.group ? " " + f.group : ""}`,
|
|
796
|
+
`${f.home.code} ${chalk.dim(f.home.name)}`,
|
|
797
|
+
`${f.away.code} ${chalk.dim(f.away.name)}`,
|
|
798
|
+
f.venue,
|
|
799
|
+
scoreStr2(f),
|
|
800
|
+
colorStatus(f.status)
|
|
801
|
+
]);
|
|
802
|
+
}
|
|
803
|
+
return t.toString();
|
|
804
|
+
}
|
|
805
|
+
function renderLivePretty(items) {
|
|
806
|
+
if (items.length === 0) return chalk.dim("no live matches");
|
|
807
|
+
const t = new Table({ head: ["Min", "Home", "Score", "Away", "Stage"], style: { head: ["bold"] } });
|
|
808
|
+
for (const m of items) {
|
|
809
|
+
t.push([
|
|
810
|
+
chalk.green(`${m.minute}'`),
|
|
811
|
+
`${m.home.code} ${chalk.dim(m.home.name)}`,
|
|
812
|
+
scoreStr2(m),
|
|
813
|
+
`${m.away.code} ${chalk.dim(m.away.name)}`,
|
|
814
|
+
stageLabel(m.stage)
|
|
815
|
+
]);
|
|
816
|
+
}
|
|
817
|
+
return t.toString();
|
|
818
|
+
}
|
|
819
|
+
function renderStandingsPretty(groups) {
|
|
820
|
+
return groups.map((g) => {
|
|
821
|
+
const t = new Table({
|
|
822
|
+
head: ["Team", "P", "W", "D", "L", "GF", "GA", "GD", "Pts"],
|
|
823
|
+
style: { head: ["bold"] }
|
|
824
|
+
});
|
|
825
|
+
for (const r of g.rows) {
|
|
826
|
+
t.push([`${r.team.code} ${chalk.dim(r.team.name)}`, r.p, r.w, r.d, r.l, r.gf, r.ga, r.gd, chalk.bold(String(r.pts))]);
|
|
827
|
+
}
|
|
828
|
+
return chalk.bold(`Group ${g.group}`) + "\n" + t.toString();
|
|
829
|
+
}).join("\n\n");
|
|
830
|
+
}
|
|
831
|
+
function renderTeamPretty(t) {
|
|
832
|
+
const head = `${chalk.bold(t.name)} (${t.code})${t.group ? ` \u2014 Group ${t.group}` : ""}${t.coach ? ` \u2014 ${t.coach}` : ""}`;
|
|
833
|
+
const sq = new Table({ head: ["#", "Name", "Pos", "Club"], style: { head: ["bold"] } });
|
|
834
|
+
for (const p of t.squad) sq.push([p.number ?? "", p.name, p.position, p.club ?? ""]);
|
|
835
|
+
const fx = renderFixturesPretty(t.fixtures);
|
|
836
|
+
return [head, sq.toString(), chalk.bold("Fixtures"), fx].join("\n");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/commands/fixtures.ts
|
|
840
|
+
function fixturesCmd(p) {
|
|
841
|
+
p.command("fixtures").description("List World Cup fixtures").option("--team <code>", "filter by FIFA 3-letter team code").option("--from <date>", 'ISO date (or "today")').option("--to <date>", "ISO date").option("--stage <s>", "group|r16|qf|sf|third|final").action(async (_opts, cmd) => {
|
|
842
|
+
const g = cmd.optsWithGlobals();
|
|
843
|
+
try {
|
|
844
|
+
const q = {
|
|
845
|
+
teamCode: g.team,
|
|
846
|
+
from: g.from === "today" ? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) : g.from,
|
|
847
|
+
to: g.to,
|
|
848
|
+
stage: g.stage
|
|
849
|
+
};
|
|
850
|
+
const key = `fixtures/${g.team ? `team-${g.team}` : "all"}`;
|
|
851
|
+
const reg = await buildRegistry(g);
|
|
852
|
+
const { data, stale, reason } = await runCached(
|
|
853
|
+
key,
|
|
854
|
+
6 * 3600,
|
|
855
|
+
async () => {
|
|
856
|
+
let provider = "";
|
|
857
|
+
const data2 = await reg.call(async (p2) => {
|
|
858
|
+
provider = p2.name;
|
|
859
|
+
return p2.fixtures(q);
|
|
860
|
+
});
|
|
861
|
+
return { data: data2, provider };
|
|
862
|
+
},
|
|
863
|
+
g
|
|
864
|
+
);
|
|
865
|
+
const filtered = data.filter(
|
|
866
|
+
(f) => (!q.stage || f.stage === q.stage) && (!q.teamCode || f.home.code === q.teamCode || f.away.code === q.teamCode)
|
|
867
|
+
);
|
|
868
|
+
if (g.json) process.stdout.write(renderJson(filtered, { stale, reason }) + "\n");
|
|
869
|
+
else if (g.plain) process.stdout.write(renderFixturesPlain(filtered));
|
|
870
|
+
else {
|
|
871
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
872
|
+
`);
|
|
873
|
+
process.stdout.write(renderFixturesPretty(filtered) + "\n");
|
|
874
|
+
}
|
|
875
|
+
} catch (e) {
|
|
876
|
+
die(e, g);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/commands/next.ts
|
|
882
|
+
init_esm_shims();
|
|
883
|
+
function nextCmd(p) {
|
|
884
|
+
p.command("next").description("Show upcoming matches").option("--team <code>", "filter by team").option("-n, --count <n>", "number of matches", "5").action(async (_opts, cmd) => {
|
|
885
|
+
const g = cmd.optsWithGlobals();
|
|
886
|
+
const n = Math.max(1, parseInt(g.count ?? "5", 10));
|
|
887
|
+
try {
|
|
888
|
+
const reg = await buildRegistry(g);
|
|
889
|
+
const key = `fixtures/${g.team ? `team-${g.team}` : "all"}`;
|
|
890
|
+
const { data, stale, reason } = await runCached(
|
|
891
|
+
key,
|
|
892
|
+
6 * 3600,
|
|
893
|
+
async () => {
|
|
894
|
+
let provider = "";
|
|
895
|
+
const data2 = await reg.call(async (p2) => {
|
|
896
|
+
provider = p2.name;
|
|
897
|
+
return p2.fixtures({ teamCode: g.team });
|
|
898
|
+
});
|
|
899
|
+
return { data: data2, provider };
|
|
900
|
+
},
|
|
901
|
+
g
|
|
902
|
+
);
|
|
903
|
+
const now = Date.now();
|
|
904
|
+
const out = data.filter((f) => f.status === "scheduled" && new Date(f.utcKickoff).getTime() > now).sort((a, b) => a.utcKickoff.localeCompare(b.utcKickoff)).slice(0, n);
|
|
905
|
+
if (g.json) process.stdout.write(renderJson(out, { stale, reason }) + "\n");
|
|
906
|
+
else if (g.plain) process.stdout.write(renderFixturesPlain(out));
|
|
907
|
+
else {
|
|
908
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
909
|
+
`);
|
|
910
|
+
process.stdout.write(renderFixturesPretty(out) + "\n");
|
|
911
|
+
}
|
|
912
|
+
} catch (e) {
|
|
913
|
+
die(e, g);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/commands/live.ts
|
|
919
|
+
init_esm_shims();
|
|
920
|
+
function liveCmd(p) {
|
|
921
|
+
p.command("live").description("Show live matches").option("--watch", "interactive dashboard").option("--interval <sec>", "refresh interval in seconds", "15").action(async (_opts, cmd) => {
|
|
922
|
+
const g = cmd.optsWithGlobals();
|
|
923
|
+
try {
|
|
924
|
+
if (g.watch) {
|
|
925
|
+
const { render } = await import("ink");
|
|
926
|
+
const React = (await import("react")).default;
|
|
927
|
+
const { Dashboard: Dashboard2 } = await Promise.resolve().then(() => (init_live_dashboard(), live_dashboard_exports));
|
|
928
|
+
const cfg = await env.config.load();
|
|
929
|
+
const interval = Math.max(10, parseInt(g.interval ?? String(cfg.defaults.watchIntervalSec), 10)) * 1e3;
|
|
930
|
+
const fav = cfg.favoriteTeam;
|
|
931
|
+
let favoriteOnly = false;
|
|
932
|
+
const reg2 = await buildRegistry(g);
|
|
933
|
+
let state = { live: [], upcoming: [], stale: false };
|
|
934
|
+
const refresh = async () => {
|
|
935
|
+
try {
|
|
936
|
+
const { data: live, stale: stale2, reason: reason2 } = await runCached(
|
|
937
|
+
"live/all",
|
|
938
|
+
10,
|
|
939
|
+
async () => {
|
|
940
|
+
let provider = "";
|
|
941
|
+
const data2 = await reg2.call(async (p2) => {
|
|
942
|
+
provider = p2.name;
|
|
943
|
+
return p2.liveMatches();
|
|
944
|
+
});
|
|
945
|
+
return { data: data2, provider };
|
|
946
|
+
},
|
|
947
|
+
g
|
|
948
|
+
);
|
|
949
|
+
const { data: fx } = await runCached(
|
|
950
|
+
"fixtures/all",
|
|
951
|
+
6 * 3600,
|
|
952
|
+
async () => {
|
|
953
|
+
let provider = "";
|
|
954
|
+
const data2 = await reg2.call(async (p2) => {
|
|
955
|
+
provider = p2.name;
|
|
956
|
+
return p2.fixtures({});
|
|
957
|
+
});
|
|
958
|
+
return { data: data2, provider };
|
|
959
|
+
},
|
|
960
|
+
g
|
|
961
|
+
);
|
|
962
|
+
const now = Date.now();
|
|
963
|
+
const upcoming = fx.filter((f) => f.status === "scheduled" && new Date(f.utcKickoff).getTime() > now).sort((a, b) => a.utcKickoff.localeCompare(b.utcKickoff)).slice(0, 8);
|
|
964
|
+
const filterFav = (m) => !favoriteOnly || !fav || m.home.code === fav || m.away.code === fav;
|
|
965
|
+
state = { live: live.filter(filterFav), upcoming: upcoming.filter(filterFav), stale: stale2, reason: reason2 };
|
|
966
|
+
app.rerender(React.createElement(Dashboard2, { ...state, favoriteOnly }));
|
|
967
|
+
} catch (e) {
|
|
968
|
+
die(e, g);
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
const app = render(React.createElement(Dashboard2, { ...state, favoriteOnly }));
|
|
972
|
+
await refresh();
|
|
973
|
+
const timer = setInterval(refresh, interval);
|
|
974
|
+
process.stdin.setRawMode?.(true);
|
|
975
|
+
process.stdin.resume();
|
|
976
|
+
const cleanup = () => {
|
|
977
|
+
clearInterval(timer);
|
|
978
|
+
process.stdin.setRawMode?.(false);
|
|
979
|
+
process.stdin.pause();
|
|
980
|
+
app.unmount();
|
|
981
|
+
};
|
|
982
|
+
process.on("SIGINT", () => {
|
|
983
|
+
cleanup();
|
|
984
|
+
process.exit(0);
|
|
985
|
+
});
|
|
986
|
+
process.on("exit", cleanup);
|
|
987
|
+
process.stdin.on("data", (buf) => {
|
|
988
|
+
const k = buf.toString();
|
|
989
|
+
if (k === "q" || k === "") {
|
|
990
|
+
cleanup();
|
|
991
|
+
process.exit(0);
|
|
992
|
+
}
|
|
993
|
+
if (k === "r") void refresh();
|
|
994
|
+
if (k === "t") {
|
|
995
|
+
favoriteOnly = !favoriteOnly;
|
|
996
|
+
void refresh();
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const reg = await buildRegistry(g);
|
|
1002
|
+
const { data, stale, reason } = await runCached(
|
|
1003
|
+
"live/all",
|
|
1004
|
+
10,
|
|
1005
|
+
async () => {
|
|
1006
|
+
let provider = "";
|
|
1007
|
+
const data2 = await reg.call(async (p2) => {
|
|
1008
|
+
provider = p2.name;
|
|
1009
|
+
return p2.liveMatches();
|
|
1010
|
+
});
|
|
1011
|
+
return { data: data2, provider };
|
|
1012
|
+
},
|
|
1013
|
+
g
|
|
1014
|
+
);
|
|
1015
|
+
if (g.json) process.stdout.write(renderJson(data, { stale, reason }) + "\n");
|
|
1016
|
+
else if (g.plain) process.stdout.write(renderLivePlain(data));
|
|
1017
|
+
else {
|
|
1018
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
1019
|
+
`);
|
|
1020
|
+
process.stdout.write(renderLivePretty(data) + "\n");
|
|
1021
|
+
}
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
die(e, g);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/commands/match.ts
|
|
1029
|
+
init_esm_shims();
|
|
1030
|
+
function renderMatchPretty(m) {
|
|
1031
|
+
const head = `[${m.stage.toUpperCase()}${m.group ? " " + m.group : ""}] ${m.home.name} vs ${m.away.name} ${m.score ? `${m.score.home}-${m.score.away}` : "\u2013"} ${m.status === "live" ? `${m.minute}'` : m.status}`;
|
|
1032
|
+
const events = m.events.map((e) => ` ${e.minute}' ${e.type.padEnd(6)} ${e.team.padEnd(4)} ${e.player ?? ""}`).join("\n");
|
|
1033
|
+
return [head, m.venue, "", "Events:", events || " (none)"].join("\n");
|
|
1034
|
+
}
|
|
1035
|
+
function renderMatchPlain(m) {
|
|
1036
|
+
const head = `match ${m.id} ${m.home.code} ${m.away.code} ${m.score ? `${m.score.home}-${m.score.away}` : "-"} ${m.status} ${m.minute}`;
|
|
1037
|
+
const events = m.events.map((e) => `event ${e.minute} ${e.type} ${e.team} ${e.player ?? "-"}`).join("\n");
|
|
1038
|
+
return events ? `${head}
|
|
1039
|
+
${events}
|
|
1040
|
+
` : `${head}
|
|
1041
|
+
`;
|
|
1042
|
+
}
|
|
1043
|
+
function matchCmd(p) {
|
|
1044
|
+
p.command("match <id>").description("Show match detail").action(async (id, _opts, cmd) => {
|
|
1045
|
+
const g = cmd.optsWithGlobals();
|
|
1046
|
+
try {
|
|
1047
|
+
const reg = await buildRegistry(g);
|
|
1048
|
+
const { data, stale, reason } = await runCached(
|
|
1049
|
+
`match/${id}`,
|
|
1050
|
+
10,
|
|
1051
|
+
async () => {
|
|
1052
|
+
let provider = "";
|
|
1053
|
+
const data2 = await reg.call(async (p2) => {
|
|
1054
|
+
provider = p2.name;
|
|
1055
|
+
return p2.match(id);
|
|
1056
|
+
});
|
|
1057
|
+
return { data: data2, provider };
|
|
1058
|
+
},
|
|
1059
|
+
g
|
|
1060
|
+
);
|
|
1061
|
+
if (g.json) process.stdout.write(renderJson(data, { stale, reason }) + "\n");
|
|
1062
|
+
else if (g.plain) process.stdout.write(renderMatchPlain(data));
|
|
1063
|
+
else {
|
|
1064
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
1065
|
+
`);
|
|
1066
|
+
process.stdout.write(renderMatchPretty(data) + "\n");
|
|
1067
|
+
}
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
die(e, g);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/commands/groups.ts
|
|
1075
|
+
init_esm_shims();
|
|
1076
|
+
function groupsCmd(p) {
|
|
1077
|
+
p.command("groups [group]").description("Show group standings").action(async (group, _opts, cmd) => {
|
|
1078
|
+
const g = cmd.optsWithGlobals();
|
|
1079
|
+
try {
|
|
1080
|
+
const reg = await buildRegistry(g);
|
|
1081
|
+
const { data, stale, reason } = await runCached(
|
|
1082
|
+
group ? `standings/group-${group.toUpperCase()}` : "standings/all",
|
|
1083
|
+
3600,
|
|
1084
|
+
async () => {
|
|
1085
|
+
let provider = "";
|
|
1086
|
+
const data2 = await reg.call(async (p2) => {
|
|
1087
|
+
provider = p2.name;
|
|
1088
|
+
return p2.standings();
|
|
1089
|
+
});
|
|
1090
|
+
return { data: data2, provider };
|
|
1091
|
+
},
|
|
1092
|
+
g
|
|
1093
|
+
);
|
|
1094
|
+
const filtered = group ? data.filter((x) => x.group === group.toUpperCase()) : data;
|
|
1095
|
+
if (g.json) process.stdout.write(renderJson(filtered, { stale, reason }) + "\n");
|
|
1096
|
+
else if (g.plain) process.stdout.write(renderStandingsPlain(filtered));
|
|
1097
|
+
else {
|
|
1098
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
1099
|
+
`);
|
|
1100
|
+
process.stdout.write(renderStandingsPretty(filtered) + "\n");
|
|
1101
|
+
}
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
die(e, g);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/commands/bracket.ts
|
|
1109
|
+
init_esm_shims();
|
|
1110
|
+
|
|
1111
|
+
// src/render/bracket.ts
|
|
1112
|
+
init_esm_shims();
|
|
1113
|
+
import chalk2 from "chalk";
|
|
1114
|
+
var STAGES = [
|
|
1115
|
+
{ key: "r16", label: "R16", slots: 8 },
|
|
1116
|
+
{ key: "qf", label: "QF", slots: 4 },
|
|
1117
|
+
{ key: "sf", label: "SF", slots: 2 },
|
|
1118
|
+
{ key: "final", label: "Final", slots: 1 },
|
|
1119
|
+
{ key: "third", label: "3rd", slots: 1 }
|
|
1120
|
+
];
|
|
1121
|
+
var COL_WIDTH = 18;
|
|
1122
|
+
function pad(s, w = COL_WIDTH) {
|
|
1123
|
+
if (s.length >= w) return s.slice(0, w);
|
|
1124
|
+
return s + " ".repeat(w - s.length);
|
|
1125
|
+
}
|
|
1126
|
+
function nodeLine(n) {
|
|
1127
|
+
if (!n) return pad("TBD vs TBD");
|
|
1128
|
+
const h = n.home?.code ?? "TBD";
|
|
1129
|
+
const a = n.away?.code ?? "TBD";
|
|
1130
|
+
const w = n.winner ? chalk2.bold(n.winner.code) : "";
|
|
1131
|
+
return pad(`${h} vs ${a}${w ? " \u2192 " + w : ""}`);
|
|
1132
|
+
}
|
|
1133
|
+
function renderBracketAscii(nodes) {
|
|
1134
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const n of nodes) {
|
|
1136
|
+
if (!byStage.has(n.stage)) byStage.set(n.stage, []);
|
|
1137
|
+
byStage.get(n.stage).push(n);
|
|
1138
|
+
}
|
|
1139
|
+
const lines = [];
|
|
1140
|
+
lines.push(STAGES.map((s) => pad(chalk2.bold(s.label))).join(" | "));
|
|
1141
|
+
lines.push(STAGES.map(() => "-".repeat(COL_WIDTH)).join("-+-"));
|
|
1142
|
+
const maxRows = Math.max(...STAGES.map((s) => s.slots));
|
|
1143
|
+
for (let i = 0; i < maxRows; i++) {
|
|
1144
|
+
const row = STAGES.map((s) => {
|
|
1145
|
+
const list = byStage.get(s.key) ?? [];
|
|
1146
|
+
if (i >= s.slots) return pad("");
|
|
1147
|
+
return nodeLine(list[i]);
|
|
1148
|
+
}).join(" | ");
|
|
1149
|
+
lines.push(row);
|
|
1150
|
+
}
|
|
1151
|
+
return lines.join("\n");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/commands/bracket.ts
|
|
1155
|
+
function bracketCmd(p) {
|
|
1156
|
+
p.command("bracket").description("Show knockout bracket").option("--from <stage>", "r16|qf|sf|final").action(async (_opts, cmd) => {
|
|
1157
|
+
const g = cmd.optsWithGlobals();
|
|
1158
|
+
try {
|
|
1159
|
+
const reg = await buildRegistry(g);
|
|
1160
|
+
const { data, stale, reason } = await runCached(
|
|
1161
|
+
"bracket/all",
|
|
1162
|
+
3600,
|
|
1163
|
+
async () => {
|
|
1164
|
+
let provider = "";
|
|
1165
|
+
const data2 = await reg.call(async (p2) => {
|
|
1166
|
+
provider = p2.name;
|
|
1167
|
+
return p2.knockoutBracket();
|
|
1168
|
+
});
|
|
1169
|
+
return { data: data2, provider };
|
|
1170
|
+
},
|
|
1171
|
+
g
|
|
1172
|
+
);
|
|
1173
|
+
const order = ["r16", "qf", "sf", "final", "third"];
|
|
1174
|
+
const fromIdx = g.from ? order.indexOf(g.from) : 0;
|
|
1175
|
+
const filtered = fromIdx <= 0 ? data : data.filter((n) => order.indexOf(n.stage) >= fromIdx);
|
|
1176
|
+
if (g.json) process.stdout.write(renderJson(filtered, { stale, reason }) + "\n");
|
|
1177
|
+
else if (g.plain) process.stdout.write(renderBracketPlain(filtered));
|
|
1178
|
+
else {
|
|
1179
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
1180
|
+
`);
|
|
1181
|
+
process.stdout.write(renderBracketAscii(filtered) + "\n");
|
|
1182
|
+
}
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
die(e, g);
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/commands/team.ts
|
|
1190
|
+
init_esm_shims();
|
|
1191
|
+
function teamCmd(p) {
|
|
1192
|
+
p.command("team <code>").description("Show team info, squad, and fixtures").action(async (code, _opts, cmd) => {
|
|
1193
|
+
const g = cmd.optsWithGlobals();
|
|
1194
|
+
const upper = code.toUpperCase();
|
|
1195
|
+
try {
|
|
1196
|
+
const reg = await buildRegistry(g);
|
|
1197
|
+
const { data, stale, reason } = await runCached(
|
|
1198
|
+
`team/${upper}`,
|
|
1199
|
+
24 * 3600,
|
|
1200
|
+
async () => {
|
|
1201
|
+
let provider = "";
|
|
1202
|
+
const data2 = await reg.call(async (p2) => {
|
|
1203
|
+
provider = p2.name;
|
|
1204
|
+
return p2.team(upper);
|
|
1205
|
+
});
|
|
1206
|
+
return { data: data2, provider };
|
|
1207
|
+
},
|
|
1208
|
+
g
|
|
1209
|
+
);
|
|
1210
|
+
if (g.json) process.stdout.write(renderJson(data, { stale, reason }) + "\n");
|
|
1211
|
+
else if (g.plain) process.stdout.write(renderTeamPlain(data));
|
|
1212
|
+
else {
|
|
1213
|
+
if (stale) process.stdout.write(`[STALE${reason ? ` ${reason}` : ""}]
|
|
1214
|
+
`);
|
|
1215
|
+
process.stdout.write(renderTeamPretty(data) + "\n");
|
|
1216
|
+
}
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
die(e, g);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/commands/config.ts
|
|
1224
|
+
init_esm_shims();
|
|
1225
|
+
function configCmd(p) {
|
|
1226
|
+
const root = p.command("config").description("Get/set config values");
|
|
1227
|
+
root.command("set <key> [args...]").description("set favorite <code> | apiKey <provider> <key> | providers <a,b>").action(async (key, args, _opts, cmd) => {
|
|
1228
|
+
const g = cmd.optsWithGlobals();
|
|
1229
|
+
try {
|
|
1230
|
+
if (key === "favorite") {
|
|
1231
|
+
if (!args[0]) throw new Error("favorite requires a team code");
|
|
1232
|
+
await env.config.set("favoriteTeam", args[0]);
|
|
1233
|
+
} else if (key === "apiKey") {
|
|
1234
|
+
if (!args[0] || !args[1]) throw new Error("apiKey requires <provider> <key>");
|
|
1235
|
+
await env.config.setApiKey(args[0], args[1]);
|
|
1236
|
+
} else if (key === "providers") {
|
|
1237
|
+
if (!args[0]) throw new Error("providers requires a comma-separated list");
|
|
1238
|
+
await env.config.set("providers", args[0].split(","));
|
|
1239
|
+
} else throw new Error(`unknown key: ${key}`);
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
die(e, g);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
root.command("get <key> [args...]").description("get a config value").action(async (key, args, _opts, cmd) => {
|
|
1245
|
+
const g = cmd.optsWithGlobals();
|
|
1246
|
+
try {
|
|
1247
|
+
const cfg = await env.config.load();
|
|
1248
|
+
if (key === "favorite") process.stdout.write((cfg.favoriteTeam ?? "") + "\n");
|
|
1249
|
+
else if (key === "apiKey") {
|
|
1250
|
+
if (!args[0]) throw new Error("apiKey requires <provider>");
|
|
1251
|
+
process.stdout.write((await env.config.apiKey(args[0]) ?? "") + "\n");
|
|
1252
|
+
} else if (key === "providers") process.stdout.write(cfg.providers.join(",") + "\n");
|
|
1253
|
+
else throw new Error(`unknown key: ${key}`);
|
|
1254
|
+
} catch (e) {
|
|
1255
|
+
die(e, g);
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/commands/cache.ts
|
|
1261
|
+
init_esm_shims();
|
|
1262
|
+
function cacheCmd(p) {
|
|
1263
|
+
const root = p.command("cache").description("Manage local cache");
|
|
1264
|
+
root.command("clear").description("drop cached files").option("--resource <name>", "only this resource directory").action(async (opts, cmd) => {
|
|
1265
|
+
const g = cmd.optsWithGlobals();
|
|
1266
|
+
try {
|
|
1267
|
+
await env.cache.clear(opts.resource);
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
die(e, g);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/cli.ts
|
|
1275
|
+
var program = new Command();
|
|
1276
|
+
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");
|
|
1277
|
+
fixturesCmd(program);
|
|
1278
|
+
nextCmd(program);
|
|
1279
|
+
liveCmd(program);
|
|
1280
|
+
matchCmd(program);
|
|
1281
|
+
groupsCmd(program);
|
|
1282
|
+
bracketCmd(program);
|
|
1283
|
+
teamCmd(program);
|
|
1284
|
+
configCmd(program);
|
|
1285
|
+
cacheCmd(program);
|
|
1286
|
+
program.parseAsync().catch((e) => {
|
|
1287
|
+
process.stderr.write(String(e) + "\n");
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
});
|