termfc 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/cli.js +3534 -0
  4. package/package.json +49 -0
package/dist/cli.js ADDED
@@ -0,0 +1,3534 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
5
+ import { join as join3 } from "path";
6
+
7
+ // src/core/i18n.ts
8
+ var LABELS = {
9
+ ko: {
10
+ live: "LIVE",
11
+ scheduled: "\uC608\uC815",
12
+ halftime: "\uD558\uD504\uD0C0\uC784",
13
+ etBreak: "\uC5F0\uC7A5 \uC900\uBE44",
14
+ penalties: "\uC2B9\uBD80\uCC28\uAE30",
15
+ finished: "\uACBD\uAE30 \uC885\uB8CC",
16
+ firstHalf: "\uC804\uBC18",
17
+ secondHalf: "\uD6C4\uBC18",
18
+ etFirst: "\uC5F0\uC7A5 \uC804\uBC18",
19
+ etSecond: "\uC5F0\uC7A5 \uD6C4\uBC18",
20
+ winprob: "\uC2B9\uB960(\uCD94\uC815)",
21
+ draw: "\uBB34",
22
+ reconnecting: "\uC5F0\uACB0 \uC7AC\uC2DC\uB3C4 \uC911",
23
+ lastUpdate: "\uB9C8\uC9C0\uB9C9 \uAC31\uC2E0",
24
+ retryIn: "\uC7AC\uC2DC\uB3C4\uAE4C\uC9C0",
25
+ offline: "\uC624\uD504\uB77C\uC778",
26
+ staleData: "\uAE30\uC900 \uB370\uC774\uD130",
27
+ nextMatchIn: "\uB2E4\uC74C \uACBD\uAE30\uAE4C\uC9C0",
28
+ noLiveMatches: "\uC9C4\uD589 \uC911\uC778 \uACBD\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
29
+ sourceSwitched: "\uB370\uC774\uD130 \uC18C\uC2A4 \uC804\uD658 \u2014 \uBC31\uC5C5 \uC18C\uC2A4\uB85C \uC911\uACC4\uB97C \uACC4\uC18D\uD569\uB2C8\uB2E4",
30
+ corrected: "\uC815\uC815",
31
+ goalCancelled: "\uACE8 \uCDE8\uC18C",
32
+ quitHint: "q \uC885\uB8CC \xB7 s \uC560\uB2C8\uBA54\uC774\uC158 \uAC74\uB108\uB6F0\uAE30",
33
+ pickPrompt: "\uBC88\uD638\uB97C \uC785\uB825\uD574 \uACBD\uAE30\uC5D0 \uC785\uC7A5\uD558\uC138\uC694 (q \uC885\uB8CC): ",
34
+ schedule: "\uACBD\uAE30 \uC77C\uC815",
35
+ liveNow: "\uC9C4\uD589 \uC911",
36
+ upcoming: "\uC608\uC815\uB41C \uACBD\uAE30",
37
+ recentResults: "\uCD5C\uADFC \uACB0\uACFC",
38
+ replaying: "\uB9AC\uD50C\uB808\uC774",
39
+ speed: "\uBC30\uC18D",
40
+ recorded: "\uB179\uD654\uB428",
41
+ loading: "\uBD88\uB7EC\uC624\uB294 \uC911...",
42
+ noData: "\uB370\uC774\uD130\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"
43
+ },
44
+ en: {
45
+ live: "LIVE",
46
+ scheduled: "Scheduled",
47
+ halftime: "Half-time",
48
+ etBreak: "Break before ET",
49
+ penalties: "Penalty shoot-out",
50
+ finished: "Full-time",
51
+ firstHalf: "1st half",
52
+ secondHalf: "2nd half",
53
+ etFirst: "ET 1st half",
54
+ etSecond: "ET 2nd half",
55
+ winprob: "Win prob (est.)",
56
+ draw: "Draw",
57
+ reconnecting: "Reconnecting",
58
+ lastUpdate: "last update",
59
+ retryIn: "retry in",
60
+ offline: "offline",
61
+ staleData: "data as of",
62
+ nextMatchIn: "Next match in",
63
+ noLiveMatches: "No matches in progress",
64
+ sourceSwitched: "Data source switched \u2014 continuing on backup source",
65
+ corrected: "corrected",
66
+ goalCancelled: "Goal disallowed",
67
+ quitHint: "q quit \xB7 s skip animation",
68
+ pickPrompt: "Enter a number to join a match (q to quit): ",
69
+ schedule: "Schedule",
70
+ liveNow: "Live now",
71
+ upcoming: "Upcoming",
72
+ recentResults: "Recent results",
73
+ replaying: "Replay",
74
+ speed: "speed",
75
+ recorded: "recorded",
76
+ loading: "Loading...",
77
+ noData: "Failed to fetch data"
78
+ }
79
+ };
80
+ function labels(lang) {
81
+ return LABELS[lang];
82
+ }
83
+ function eventSentence(e, lang, ctx = {}) {
84
+ const who = ctx.player;
85
+ const team = ctx.team;
86
+ const koWho = who ? `${who}${team ? ` (${team})` : ""}` : team ?? "";
87
+ const enWho = who ? `${who}${team ? ` (${team})` : ""}` : team ?? "";
88
+ if (lang === "ko") {
89
+ switch (e.type) {
90
+ case "GOAL":
91
+ return koWho ? `\uACE8! ${koWho} \uB4DD\uC810!` : "\uACE8!";
92
+ case "OWN_GOAL":
93
+ return koWho ? `\uC790\uCC45\uACE8! ${koWho}` : "\uC790\uCC45\uACE8!";
94
+ case "PENALTY_GOAL":
95
+ return koWho ? `${koWho} \uD398\uB110\uD2F0\uD0A5 \uC131\uACF5!` : "\uD398\uB110\uD2F0\uD0A5 \uC131\uACF5!";
96
+ case "PENALTY_MISS":
97
+ return koWho ? `${koWho} \uD398\uB110\uD2F0\uD0A5 \uC2E4\uCD95` : "\uD398\uB110\uD2F0\uD0A5 \uC2E4\uCD95";
98
+ case "ASSIST":
99
+ return koWho ? `${koWho} \uC5B4\uC2DC\uC2A4\uD2B8` : "\uC5B4\uC2DC\uC2A4\uD2B8";
100
+ case "SHOT":
101
+ return koWho ? `${koWho} \uC288\uD305 \uC2DC\uB3C4` : "\uC288\uD305 \uC2DC\uB3C4";
102
+ case "SAVE":
103
+ return team ? `${team} \uACE8\uD0A4\uD37C \uC120\uBC29` : "\uACE8\uD0A4\uD37C \uC120\uBC29";
104
+ case "FOUL":
105
+ return koWho ? `${koWho} \uD30C\uC6B8` : "\uD30C\uC6B8";
106
+ case "YELLOW":
107
+ return koWho ? `${koWho} \uACBD\uACE0 (\uC610\uB85C\uCE74\uB4DC)` : "\uACBD\uACE0 (\uC610\uB85C\uCE74\uB4DC)";
108
+ case "RED":
109
+ return koWho ? `${koWho} \uD1F4\uC7A5! (\uB808\uB4DC\uCE74\uB4DC)` : "\uD1F4\uC7A5! (\uB808\uB4DC\uCE74\uB4DC)";
110
+ case "CORNER":
111
+ return team ? `${team} \uCF54\uB108\uD0A5` : "\uCF54\uB108\uD0A5";
112
+ case "OFFSIDE":
113
+ return koWho ? `${koWho} \uC624\uD504\uC0AC\uC774\uB4DC` : "\uC624\uD504\uC0AC\uC774\uB4DC";
114
+ case "SUBSTITUTION":
115
+ return team ? `${team} \uC120\uC218 \uAD50\uCCB4` : "\uC120\uC218 \uAD50\uCCB4";
116
+ case "VAR":
117
+ return "VAR \uD310\uB3C5 \uC911";
118
+ case "PERIOD_START":
119
+ return "\uACBD\uAE30 \uC2DC\uC791 \uD718\uC2AC";
120
+ case "PERIOD_END":
121
+ return "\uC885\uB8CC \uD718\uC2AC";
122
+ case "FULLTIME":
123
+ return "\uACBD\uAE30 \uC885\uB8CC";
124
+ case "COIN_TOSS":
125
+ return "\uB3D9\uC804 \uB358\uC9C0\uAE30";
126
+ case "BREAK":
127
+ return "\uACBD\uAE30 \uC77C\uC2DC \uC911\uB2E8";
128
+ case "RESUMED":
129
+ return "\uACBD\uAE30 \uC7AC\uAC1C";
130
+ default:
131
+ return "\uACBD\uAE30 \uC0C1\uD669";
132
+ }
133
+ }
134
+ switch (e.type) {
135
+ case "GOAL":
136
+ return enWho ? `Goal! ${enWho} scores!` : "Goal!";
137
+ case "OWN_GOAL":
138
+ return enWho ? `Own goal by ${enWho}` : "Own goal";
139
+ case "PENALTY_GOAL":
140
+ return enWho ? `${enWho} converts the penalty!` : "Penalty scored!";
141
+ case "PENALTY_MISS":
142
+ return enWho ? `${enWho} misses the penalty` : "Penalty missed";
143
+ case "ASSIST":
144
+ return enWho ? `Assisted by ${enWho}` : "Assist";
145
+ case "SHOT":
146
+ return enWho ? `${enWho} attempts a shot` : "Shot attempt";
147
+ case "SAVE":
148
+ return team ? `Save by the ${team} goalkeeper` : "Goalkeeper save";
149
+ case "FOUL":
150
+ return enWho ? `Foul by ${enWho}` : "Foul";
151
+ case "YELLOW":
152
+ return enWho ? `${enWho} is booked` : "Yellow card";
153
+ case "RED":
154
+ return enWho ? `${enWho} is sent off!` : "Red card!";
155
+ case "CORNER":
156
+ return team ? `Corner kick for ${team}` : "Corner kick";
157
+ case "OFFSIDE":
158
+ return enWho ? `${enWho} is offside` : "Offside";
159
+ case "SUBSTITUTION":
160
+ return team ? `Substitution for ${team}` : "Substitution";
161
+ case "VAR":
162
+ return "VAR review";
163
+ case "PERIOD_START":
164
+ return "The referee starts the period";
165
+ case "PERIOD_END":
166
+ return "End of the period";
167
+ case "FULLTIME":
168
+ return "Full-time";
169
+ case "COIN_TOSS":
170
+ return "Coin toss";
171
+ case "BREAK":
172
+ return "Match paused";
173
+ case "RESUMED":
174
+ return "Match resumed";
175
+ default:
176
+ return "Match event";
177
+ }
178
+ }
179
+ var EVENT_ICON = {
180
+ GOAL: "\u26BD",
181
+ OWN_GOAL: "\u26BD",
182
+ PENALTY_GOAL: "\u26BD",
183
+ PENALTY_MISS: "\u274C",
184
+ ASSIST: "\u{1F170}\uFE0F ",
185
+ SHOT: "\u{1F3AF}",
186
+ SAVE: "\u{1F9E4}",
187
+ FOUL: "\u26A0\uFE0F ",
188
+ YELLOW: "\u{1F7E8}",
189
+ RED: "\u{1F7E5}",
190
+ CORNER: "\u26F3",
191
+ OFFSIDE: "\u{1F6A9}",
192
+ SUBSTITUTION: "\u{1F501}",
193
+ VAR: "\u{1F4FA}",
194
+ PERIOD_START: "\u{1F7E2}",
195
+ PERIOD_END: "\u23F8 ",
196
+ FULLTIME: "\u{1F3C1}",
197
+ COIN_TOSS: "\u{1FA99}",
198
+ BREAK: "\u{1F4A7}",
199
+ RESUMED: "\u25B6\uFE0F ",
200
+ UNKNOWN: "\xB7"
201
+ };
202
+
203
+ // src/core/model.ts
204
+ var HIGH_SALIENCE = /* @__PURE__ */ new Set([
205
+ "GOAL",
206
+ "OWN_GOAL",
207
+ "PENALTY_GOAL",
208
+ "PENALTY_MISS",
209
+ "RED",
210
+ "VAR"
211
+ ]);
212
+ var CELEBRATION_TYPES = /* @__PURE__ */ new Set([
213
+ "GOAL",
214
+ "OWN_GOAL",
215
+ "PENALTY_GOAL"
216
+ ]);
217
+ function salienceOf(type) {
218
+ return HIGH_SALIENCE.has(type) ? "high" : "low";
219
+ }
220
+ function compareEvents(a, b) {
221
+ return (a.period ?? 0) - (b.period ?? 0) || a.minute - b.minute || (a.injury ?? 0) - (b.injury ?? 0) || (a.second ?? 0) - (b.second ?? 0) || a.seq - b.seq;
222
+ }
223
+ function parseMatchMinute(raw) {
224
+ if (!raw) return { minute: 0 };
225
+ const m = /^(\d+)'(?:\s*\+\s*(\d+)')?/.exec(raw.trim());
226
+ if (!m) return { minute: 0 };
227
+ const minute = Number(m[1]);
228
+ const injury = m[2] !== void 0 ? Number(m[2]) : void 0;
229
+ return injury !== void 0 ? { minute, injury } : { minute };
230
+ }
231
+ function formatEventClock(e) {
232
+ return e.injury !== void 0 && e.injury > 0 ? `${e.minute}'+${e.injury}'` : `${e.minute}'`;
233
+ }
234
+ function isLivePhase(phase) {
235
+ return phase === "FIRST_HALF" || phase === "SECOND_HALF" || phase === "ET_FIRST" || phase === "ET_SECOND" || phase === "PENALTIES" || phase === "HALFTIME" || phase === "ET_BREAK";
236
+ }
237
+ function isClockRunning(phase) {
238
+ return phase === "FIRST_HALF" || phase === "SECOND_HALF" || phase === "ET_FIRST" || phase === "ET_SECOND";
239
+ }
240
+ function regulationCap(phase) {
241
+ switch (phase) {
242
+ case "FIRST_HALF":
243
+ return 45;
244
+ case "SECOND_HALF":
245
+ return 90;
246
+ case "ET_FIRST":
247
+ return 105;
248
+ case "ET_SECOND":
249
+ return 120;
250
+ default:
251
+ return void 0;
252
+ }
253
+ }
254
+
255
+ // src/core/state.ts
256
+ function mapFifaPhase(matchStatus, period, minute) {
257
+ if (matchStatus === 0) return "FINISHED";
258
+ if (matchStatus === 1) return "SCHEDULED";
259
+ if (matchStatus === 4) return "ABANDONED";
260
+ switch (period) {
261
+ case 3:
262
+ return "FIRST_HALF";
263
+ case 4:
264
+ return "HALFTIME";
265
+ case 5:
266
+ return "SECOND_HALF";
267
+ case 7:
268
+ return "ET_FIRST";
269
+ case 8:
270
+ return "ET_BREAK";
271
+ case 9:
272
+ return "ET_SECOND";
273
+ case 10:
274
+ return "FINISHED";
275
+ case 11:
276
+ return "PENALTIES";
277
+ }
278
+ if (minute > 90) return "ET_FIRST";
279
+ return minute > 45 ? "SECOND_HALF" : "FIRST_HALF";
280
+ }
281
+ function mapEspnPhase(statusName, state, period) {
282
+ switch (statusName) {
283
+ case "STATUS_SCHEDULED":
284
+ return "SCHEDULED";
285
+ case "STATUS_HALFTIME":
286
+ return "HALFTIME";
287
+ case "STATUS_FULL_TIME":
288
+ case "STATUS_FINAL":
289
+ case "STATUS_FINAL_PEN":
290
+ return "FINISHED";
291
+ case "STATUS_ABANDONED":
292
+ case "STATUS_CANCELED":
293
+ case "STATUS_POSTPONED":
294
+ return "ABANDONED";
295
+ case "STATUS_END_OF_REGULATION":
296
+ case "STATUS_END_OF_EXTRATIME":
297
+ return "ET_BREAK";
298
+ case "STATUS_SHOOTOUT":
299
+ return "PENALTIES";
300
+ case "STATUS_OVERTIME":
301
+ return period >= 4 ? "ET_SECOND" : "ET_FIRST";
302
+ case "STATUS_FIRST_HALF":
303
+ return "FIRST_HALF";
304
+ case "STATUS_SECOND_HALF":
305
+ return "SECOND_HALF";
306
+ case "STATUS_IN_PROGRESS":
307
+ if (period <= 1) return "FIRST_HALF";
308
+ if (period === 2) return "SECOND_HALF";
309
+ return period === 3 ? "ET_FIRST" : "ET_SECOND";
310
+ }
311
+ if (state === "post") return "FINISHED";
312
+ if (state === "pre") return "SCHEDULED";
313
+ return "UNKNOWN";
314
+ }
315
+ function phaseLabel(phase, l) {
316
+ switch (phase) {
317
+ case "SCHEDULED":
318
+ return l.scheduled;
319
+ case "FIRST_HALF":
320
+ return l.firstHalf;
321
+ case "HALFTIME":
322
+ return l.halftime;
323
+ case "SECOND_HALF":
324
+ return l.secondHalf;
325
+ case "ET_FIRST":
326
+ return l.etFirst;
327
+ case "ET_BREAK":
328
+ return l.etBreak;
329
+ case "ET_SECOND":
330
+ return l.etSecond;
331
+ case "PENALTIES":
332
+ return l.penalties;
333
+ case "FINISHED":
334
+ return l.finished;
335
+ case "ABANDONED":
336
+ return "\u2014";
337
+ default:
338
+ return "";
339
+ }
340
+ }
341
+
342
+ // src/data/espn-event-map.json
343
+ var espn_event_map_default = {
344
+ _comment: "ESPN site.api.espn.com commentary play.type.type -> internal EventType. Note: ESPN numeric type id 70 is Goal (unrelated to FIFA code 0). Keyed by the slug string for stability.",
345
+ goal: "GOAL",
346
+ "own-goal": "OWN_GOAL",
347
+ "penalty--scored": "PENALTY_GOAL",
348
+ "penalty--missed": "PENALTY_MISS",
349
+ "penalty--saved": "PENALTY_MISS",
350
+ "shot-on-goal": "SHOT",
351
+ "attempt-saved": "SAVE",
352
+ "attempt-blocked": "SHOT",
353
+ "attempt-missed": "SHOT",
354
+ post: "SHOT",
355
+ foul: "FOUL",
356
+ "yellow-card": "YELLOW",
357
+ "red-card": "RED",
358
+ "second-yellow-card": "RED",
359
+ "corner-kick": "CORNER",
360
+ offside: "OFFSIDE",
361
+ substitution: "SUBSTITUTION",
362
+ "video-review": "VAR",
363
+ var: "VAR",
364
+ kickoff: "PERIOD_START",
365
+ "half-time": "PERIOD_END",
366
+ halftime: "PERIOD_END",
367
+ "end-regular-time": "PERIOD_END",
368
+ "full-time": "FULLTIME",
369
+ "start-delay": "BREAK",
370
+ "end-delay": "RESUMED"
371
+ };
372
+
373
+ // src/data/provider.ts
374
+ var SchemaError = class extends Error {
375
+ constructor(source, message) {
376
+ super(`[${source}] ${message}`);
377
+ this.source = source;
378
+ this.name = "SchemaError";
379
+ }
380
+ source;
381
+ };
382
+ var POLITE_HEADERS = {
383
+ "user-agent": "termfc (+https://github.com/juunghyun/termfc)",
384
+ accept: "application/json"
385
+ };
386
+ async function getJson(url, timeoutMs = 12e3) {
387
+ const res = await fetch(url, {
388
+ headers: POLITE_HEADERS,
389
+ signal: AbortSignal.timeout(timeoutMs)
390
+ });
391
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`);
392
+ return res.json();
393
+ }
394
+
395
+ // src/data/teams.ts
396
+ var FIFA_TO_ISO2 = {
397
+ // Hosts + Americas
398
+ USA: "us",
399
+ MEX: "mx",
400
+ CAN: "ca",
401
+ ARG: "ar",
402
+ BRA: "br",
403
+ URU: "uy",
404
+ COL: "co",
405
+ ECU: "ec",
406
+ PAR: "py",
407
+ CHI: "cl",
408
+ PER: "pe",
409
+ VEN: "ve",
410
+ BOL: "bo",
411
+ PAN: "pa",
412
+ CRC: "cr",
413
+ HON: "hn",
414
+ SLV: "sv",
415
+ GUA: "gt",
416
+ JAM: "jm",
417
+ HAI: "ht",
418
+ CUW: "cw",
419
+ TRI: "tt",
420
+ CUB: "cu",
421
+ // Europe
422
+ ENG: "gb-eng",
423
+ SCO: "gb-sct",
424
+ WAL: "gb-wls",
425
+ NIR: "gb-nir",
426
+ FRA: "fr",
427
+ GER: "de",
428
+ ESP: "es",
429
+ POR: "pt",
430
+ ITA: "it",
431
+ NED: "nl",
432
+ BEL: "be",
433
+ CRO: "hr",
434
+ SUI: "ch",
435
+ AUT: "at",
436
+ DEN: "dk",
437
+ SWE: "se",
438
+ NOR: "no",
439
+ POL: "pl",
440
+ CZE: "cz",
441
+ SVK: "sk",
442
+ SVN: "si",
443
+ SRB: "rs",
444
+ UKR: "ua",
445
+ ROU: "ro",
446
+ HUN: "hu",
447
+ GRE: "gr",
448
+ TUR: "tr",
449
+ IRL: "ie",
450
+ ISL: "is",
451
+ FIN: "fi",
452
+ ALB: "al",
453
+ BIH: "ba",
454
+ MKD: "mk",
455
+ GEO: "ge",
456
+ // Africa
457
+ MAR: "ma",
458
+ SEN: "sn",
459
+ TUN: "tn",
460
+ ALG: "dz",
461
+ EGY: "eg",
462
+ NGA: "ng",
463
+ GHA: "gh",
464
+ CMR: "cm",
465
+ CIV: "ci",
466
+ MLI: "ml",
467
+ BFA: "bf",
468
+ RSA: "za",
469
+ COD: "cd",
470
+ CPV: "cv",
471
+ GAB: "ga",
472
+ GUI: "gn",
473
+ ZAM: "zm",
474
+ ANG: "ao",
475
+ // Asia / Oceania
476
+ KOR: "kr",
477
+ JPN: "jp",
478
+ KSA: "sa",
479
+ IRN: "ir",
480
+ AUS: "au",
481
+ QAT: "qa",
482
+ UAE: "ae",
483
+ IRQ: "iq",
484
+ UZB: "uz",
485
+ JOR: "jo",
486
+ OMA: "om",
487
+ BHR: "bh",
488
+ KUW: "kw",
489
+ CHN: "cn",
490
+ IND: "in",
491
+ THA: "th",
492
+ VIE: "vn",
493
+ IDN: "id",
494
+ MAS: "my",
495
+ NZL: "nz",
496
+ FIJ: "fj"
497
+ };
498
+ function flagEmoji(fifaCode) {
499
+ if (!fifaCode) return "\u{1F3F3}\uFE0F";
500
+ const iso = FIFA_TO_ISO2[fifaCode.toUpperCase()];
501
+ if (!iso) return "\u{1F3F3}\uFE0F";
502
+ if (iso.startsWith("gb-")) {
503
+ const tags = {
504
+ "gb-eng": "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}",
505
+ "gb-sct": "\u{1F3F4}\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}",
506
+ "gb-wls": "\u{1F3F4}\u{E0067}\u{E0062}\u{E0077}\u{E006C}\u{E0073}\u{E007F}",
507
+ "gb-nir": "\u{1F1EC}\u{1F1E7}"
508
+ };
509
+ return tags[iso] ?? "\u{1F1EC}\u{1F1E7}";
510
+ }
511
+ const A = 127462;
512
+ const chars = iso.toUpperCase().split("").map((c) => String.fromCodePoint(A + c.charCodeAt(0) - 65));
513
+ return chars.join("");
514
+ }
515
+
516
+ // src/data/espn.ts
517
+ var ESPN_BASE = "https://site.api.espn.com/apis/site/v2/sports/soccer";
518
+ var BASE = ESPN_BASE;
519
+ var ESPN_WC_SLUG = "fifa.world";
520
+ function fmtDate(d) {
521
+ return d.toISOString().slice(0, 10).replaceAll("-", "");
522
+ }
523
+ var EspnProvider = class {
524
+ constructor(league = ESPN_WC_SLUG, base = BASE) {
525
+ this.league = league;
526
+ this.base = base;
527
+ }
528
+ league;
529
+ base;
530
+ name = "espn";
531
+ scoreboardUrl() {
532
+ const from = fmtDate(new Date(Date.now() - 36 * 36e5));
533
+ const to = fmtDate(new Date(Date.now() + 8 * 864e5));
534
+ return `${this.base}/${this.league}/scoreboard?dates=${from}-${to}`;
535
+ }
536
+ async fetchSchedule(_lang) {
537
+ const data = await getJson(this.scoreboardUrl());
538
+ if (!Array.isArray(data?.events))
539
+ throw new SchemaError("espn", "scoreboard events is not an array");
540
+ return data.events.map((e) => this.normalizeEvent(e)).filter((m) => m !== null).sort((a, b) => a.kickoff.localeCompare(b.kickoff));
541
+ }
542
+ normalizeEvent(e) {
543
+ const comp = e.competitions?.[0];
544
+ if (!comp) return null;
545
+ const homeC = comp.competitors?.find((c) => c.homeAway === "home");
546
+ const awayC = comp.competitors?.find((c) => c.homeAway === "away");
547
+ if (!homeC || !awayC) return null;
548
+ const status = comp.status ?? e.status ?? {};
549
+ const phase = mapEspnPhase(
550
+ status.type?.name ?? "",
551
+ status.type?.state ?? "",
552
+ status.period ?? 0
553
+ );
554
+ const team = (c) => {
555
+ const code = c.team?.abbreviation ?? "TBD";
556
+ return {
557
+ code,
558
+ name: c.team?.displayName ?? code,
559
+ flag: flagEmoji(code)
560
+ };
561
+ };
562
+ return {
563
+ id: String(e.id),
564
+ stage: e.season?.slug ?? comp.altGameNote ?? "",
565
+ kickoff: comp.date ?? e.date,
566
+ home: team(homeC),
567
+ away: team(awayC),
568
+ score: {
569
+ home: Number(homeC.score ?? 0),
570
+ away: Number(awayC.score ?? 0)
571
+ },
572
+ phase,
573
+ matchTime: status.displayClock,
574
+ sourceRefs: { espn: { eventId: String(e.id) } }
575
+ };
576
+ }
577
+ async fetchMatchState(match, _lang) {
578
+ const ref = match.sourceRefs.espn;
579
+ if (!ref) return null;
580
+ const data = await getJson(this.scoreboardUrl());
581
+ const e = (data?.events ?? []).find(
582
+ (ev) => String(ev.id) === ref.eventId
583
+ );
584
+ if (!e) return null;
585
+ const comp = e.competitions?.[0];
586
+ const status = comp?.status ?? e.status ?? {};
587
+ const homeC = comp?.competitors?.find((c) => c.homeAway === "home");
588
+ const awayC = comp?.competitors?.find((c) => c.homeAway === "away");
589
+ const clockSec = typeof status.clock === "number" ? status.clock : void 0;
590
+ const { injury } = parseMatchMinute(status.displayClock);
591
+ return {
592
+ score: {
593
+ home: Number(homeC?.score ?? 0),
594
+ away: Number(awayC?.score ?? 0)
595
+ },
596
+ phase: mapEspnPhase(
597
+ status.type?.name ?? "",
598
+ status.type?.state ?? "",
599
+ status.period ?? 0
600
+ ),
601
+ minute: clockSec !== void 0 ? Math.floor(clockSec / 60) : parseMatchMinute(status.displayClock).minute,
602
+ ...clockSec !== void 0 ? { second: Math.floor(clockSec % 60) } : {},
603
+ ...injury !== void 0 ? { injury } : {}
604
+ };
605
+ }
606
+ /**
607
+ * ESPN commentary is copyrighted prose — we never redistribute it. We map
608
+ * the structured play data (type, clock, team, players) to internal events
609
+ * and generate our own sentences via i18n templates.
610
+ */
611
+ async fetchTimeline(match, lang) {
612
+ const ref = match.sourceRefs.espn;
613
+ if (!ref) throw new SchemaError("espn", "match has no espn source ref");
614
+ const data = await getJson(
615
+ `${this.base}/${this.league}/summary?event=${ref.eventId}`
616
+ );
617
+ const commentary = data?.commentary;
618
+ if (!Array.isArray(commentary))
619
+ throw new SchemaError("espn", "summary commentary is not an array");
620
+ const sideByName = /* @__PURE__ */ new Map();
621
+ for (const c of data?.header?.competitions?.[0]?.competitors ?? []) {
622
+ const side = c.homeAway === "home" ? "home" : "away";
623
+ for (const n of [
624
+ c.team?.displayName,
625
+ c.team?.shortDisplayName,
626
+ c.team?.name
627
+ ])
628
+ if (n) sideByName.set(n, side);
629
+ }
630
+ const map = espn_event_map_default;
631
+ return commentary.map((item, i) => {
632
+ const play = item.play ?? {};
633
+ const slug = play.type?.type ?? "";
634
+ const type = map[slug] ?? "UNKNOWN";
635
+ const totalSec = typeof item.time?.value === "number" ? item.time.value : 0;
636
+ const { minute: dispMin, injury } = parseMatchMinute(
637
+ item.time?.displayValue
638
+ );
639
+ const minute = dispMin || Math.floor(totalSec / 60);
640
+ const teamName = play.team?.displayName;
641
+ const teamSide = teamName ? sideByName.get(teamName) : void 0;
642
+ const player = play.participants?.[0]?.athlete?.displayName;
643
+ return {
644
+ id: `espn:${play.id ?? `c${item.sequence ?? i}`}`,
645
+ type,
646
+ minute,
647
+ ...injury !== void 0 ? { injury } : {},
648
+ second: Math.floor(totalSec % 60),
649
+ period: play.period?.number,
650
+ ...teamSide ? { teamSide, teamCode: match[teamSide].code } : {},
651
+ ...player ? { player } : {},
652
+ text: eventSentence({ type }, lang, {
653
+ player,
654
+ team: teamSide ? match[teamSide].name : teamName
655
+ }),
656
+ source: "espn",
657
+ seq: i
658
+ };
659
+ });
660
+ }
661
+ };
662
+
663
+ // src/data/failover.ts
664
+ var FailoverProvider = class {
665
+ constructor(primary, secondary, opts = {}) {
666
+ this.primary = primary;
667
+ this.secondary = secondary;
668
+ this.opts = opts;
669
+ this.active = primary;
670
+ this.threshold = opts.threshold ?? 3;
671
+ }
672
+ primary;
673
+ secondary;
674
+ opts;
675
+ fails = 0;
676
+ active;
677
+ threshold;
678
+ get name() {
679
+ return this.active.name;
680
+ }
681
+ get activeSource() {
682
+ return this.active.name;
683
+ }
684
+ fetchSchedule(lang) {
685
+ return this.call((p) => p.fetchSchedule(lang));
686
+ }
687
+ fetchMatchState(match, lang) {
688
+ return this.call((p) => p.fetchMatchState(match, lang));
689
+ }
690
+ fetchTimeline(match, lang) {
691
+ return this.call((p) => p.fetchTimeline(match, lang));
692
+ }
693
+ async call(fn) {
694
+ try {
695
+ const result = await fn(this.active);
696
+ this.fails = 0;
697
+ return result;
698
+ } catch (err) {
699
+ this.fails++;
700
+ const schemaBroken = err instanceof SchemaError;
701
+ const canSwitch = this.secondary !== null && this.active === this.primary;
702
+ if (canSwitch && (schemaBroken || this.fails >= this.threshold)) {
703
+ this.active = this.secondary;
704
+ this.fails = 0;
705
+ this.opts.onSwitch?.(this.active.name);
706
+ return fn(this.active);
707
+ }
708
+ throw err;
709
+ }
710
+ }
711
+ };
712
+ function linkSourceRefs(primaryMatches, espnMatches) {
713
+ const byPair = /* @__PURE__ */ new Map();
714
+ for (const em of espnMatches) {
715
+ const key = [em.home.code, em.away.code].sort().join("|");
716
+ const list = byPair.get(key) ?? [];
717
+ list.push(em);
718
+ byPair.set(key, list);
719
+ }
720
+ for (const m of primaryMatches) {
721
+ if (m.sourceRefs.espn) continue;
722
+ const key = [m.home.code, m.away.code].sort().join("|");
723
+ const candidates = byPair.get(key);
724
+ if (!candidates?.length) continue;
725
+ const k = new Date(m.kickoff).getTime();
726
+ const best = candidates.reduce(
727
+ (a, b) => Math.abs(new Date(a.kickoff).getTime() - k) <= Math.abs(new Date(b.kickoff).getTime() - k) ? a : b
728
+ );
729
+ if (Math.abs(new Date(best.kickoff).getTime() - k) <= 90 * 6e4) {
730
+ m.sourceRefs.espn = best.sourceRefs.espn;
731
+ }
732
+ }
733
+ }
734
+
735
+ // src/data/fifa-event-map.json
736
+ var fifa_event_map_default = {
737
+ _comment: "FIFA api.fifa.com timeline Event.Type -> internal EventType. Verified against real 2026 World Cup dumps (12 finished matches, 2026-07-07). Unmapped codes fall through as UNKNOWN with text passthrough.",
738
+ "0": "GOAL",
739
+ "1": "ASSIST",
740
+ "2": "YELLOW",
741
+ "3": "RED",
742
+ "5": "SUBSTITUTION",
743
+ "7": "PERIOD_START",
744
+ "8": "PERIOD_END",
745
+ "12": "SHOT",
746
+ "15": "OFFSIDE",
747
+ "16": "CORNER",
748
+ "18": "FOUL",
749
+ "26": "FULLTIME",
750
+ "34": "OWN_GOAL",
751
+ "41": "PENALTY_GOAL",
752
+ "57": "SAVE",
753
+ "60": "PENALTY_MISS",
754
+ "71": "VAR",
755
+ "78": "RESUMED",
756
+ "79": "COIN_TOSS",
757
+ "80": "COIN_TOSS",
758
+ "82": "COIN_TOSS",
759
+ "83": "BREAK"
760
+ };
761
+
762
+ // src/data/fifa.ts
763
+ var BASE2 = "https://api.fifa.com/api/v3";
764
+ var WORLD_CUP_2026 = { idCompetition: "17", idSeason: "285023" };
765
+ function apiLang(lang) {
766
+ return lang === "ko" ? "ko" : "en";
767
+ }
768
+ function isoFloorHour(ms) {
769
+ const d = new Date(ms);
770
+ d.setUTCMinutes(0, 0, 0);
771
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
772
+ }
773
+ function toTeam(raw, placeholder) {
774
+ if (!raw) return { code: "TBD", name: placeholder || "TBD", flag: "\u{1F3F3}\uFE0F" };
775
+ const code = raw.Abbreviation ?? "TBD";
776
+ const name = raw.TeamName?.[0]?.Description ?? raw.ShortClubName ?? code;
777
+ return { code, name, flag: flagEmoji(code) };
778
+ }
779
+ function normalizeMatch(m) {
780
+ const { minute } = parseMatchMinute(m.MatchTime);
781
+ const phase = mapFifaPhase(m.MatchStatus, m.Period, minute);
782
+ const home = toTeam(m.Home, m.PlaceHolderA ?? "TBD");
783
+ const away = toTeam(m.Away, m.PlaceHolderB ?? "TBD");
784
+ const match = {
785
+ id: String(m.IdMatch),
786
+ stage: m.StageName?.[0]?.Description ?? "",
787
+ kickoff: m.Date,
788
+ home,
789
+ away,
790
+ score: {
791
+ home: m.Home?.Score ?? m.HomeTeamScore ?? 0,
792
+ away: m.Away?.Score ?? m.AwayTeamScore ?? 0,
793
+ ...m.HomeTeamPenaltyScore || m.AwayTeamPenaltyScore ? {
794
+ penHome: m.HomeTeamPenaltyScore ?? 0,
795
+ penAway: m.AwayTeamPenaltyScore ?? 0
796
+ } : {}
797
+ },
798
+ phase,
799
+ matchTime: m.MatchTime || void 0,
800
+ sourceRefs: {
801
+ fifa: {
802
+ idCompetition: String(m.IdCompetition),
803
+ idSeason: String(m.IdSeason),
804
+ idStage: String(m.IdStage),
805
+ idMatch: String(m.IdMatch),
806
+ idHomeTeam: m.Home?.IdTeam ? String(m.Home.IdTeam) : void 0,
807
+ idAwayTeam: m.Away?.IdTeam ? String(m.Away.IdTeam) : void 0
808
+ }
809
+ }
810
+ };
811
+ return match;
812
+ }
813
+ var PLAYER_TYPES = /* @__PURE__ */ new Set([
814
+ "GOAL",
815
+ "OWN_GOAL",
816
+ "PENALTY_GOAL",
817
+ "PENALTY_MISS",
818
+ "YELLOW",
819
+ "RED",
820
+ "SHOT",
821
+ "OFFSIDE",
822
+ "FOUL"
823
+ ]);
824
+ function extractPlayer(text) {
825
+ if (!text) return void 0;
826
+ const m = /^(.{2,40}?)\s*\(/.exec(text);
827
+ return m?.[1]?.trim() || void 0;
828
+ }
829
+ var FifaProvider = class {
830
+ constructor(season = WORLD_CUP_2026, base = BASE2) {
831
+ this.season = season;
832
+ this.base = base;
833
+ }
834
+ season;
835
+ base;
836
+ name = "fifa";
837
+ async fetchSchedule(lang) {
838
+ const now = Date.now();
839
+ const from = isoFloorHour(now - 36 * 36e5);
840
+ const to = isoFloorHour(now + 8 * 864e5 + 36e5);
841
+ const data = await getJson(
842
+ `${this.base}/calendar/matches?from=${from}&to=${to}&idCompetition=${this.season.idCompetition}&idSeason=${this.season.idSeason}&count=200&language=${apiLang(lang)}`
843
+ );
844
+ if (!Array.isArray(data?.Results))
845
+ throw new SchemaError("fifa", "calendar Results is not an array");
846
+ return data.Results.map(normalizeMatch).sort(
847
+ (a, b) => a.kickoff.localeCompare(b.kickoff)
848
+ );
849
+ }
850
+ async fetchMatchState(match, lang) {
851
+ const ref = match.sourceRefs.fifa;
852
+ if (!ref) return null;
853
+ try {
854
+ const live = await getJson(
855
+ `${this.base}/live/football/now?language=${apiLang(lang)}`
856
+ );
857
+ const m2 = (live?.Results ?? []).find(
858
+ (r) => String(r.IdMatch) === ref.idMatch
859
+ );
860
+ if (m2) return this.toState(m2);
861
+ } catch {
862
+ }
863
+ const k = new Date(match.kickoff).getTime();
864
+ const from = isoFloorHour(k - 36e5);
865
+ const to = isoFloorHour(k + 9 * 36e5);
866
+ const data = await getJson(
867
+ `${this.base}/calendar/matches?from=${from}&to=${to}&idCompetition=${this.season.idCompetition}&idSeason=${this.season.idSeason}&count=50&language=${apiLang(lang)}`
868
+ );
869
+ const m = (data?.Results ?? []).find(
870
+ (r) => String(r.IdMatch) === ref.idMatch
871
+ );
872
+ return m ? this.toState(m) : null;
873
+ }
874
+ toState(m) {
875
+ const { minute, injury } = parseMatchMinute(m.MatchTime);
876
+ return {
877
+ score: {
878
+ home: m.Home?.Score ?? m.HomeTeamScore ?? 0,
879
+ away: m.Away?.Score ?? m.AwayTeamScore ?? 0,
880
+ ...m.HomeTeamPenaltyScore || m.AwayTeamPenaltyScore ? {
881
+ penHome: m.HomeTeamPenaltyScore ?? 0,
882
+ penAway: m.AwayTeamPenaltyScore ?? 0
883
+ } : {}
884
+ },
885
+ phase: mapFifaPhase(m.MatchStatus, m.Period, minute),
886
+ minute,
887
+ ...injury !== void 0 ? { injury } : {}
888
+ };
889
+ }
890
+ async fetchTimeline(match, lang) {
891
+ const ref = match.sourceRefs.fifa;
892
+ if (!ref) throw new SchemaError("fifa", "match has no fifa source ref");
893
+ const data = await getJson(
894
+ `${this.base}/timelines/${ref.idCompetition}/${ref.idSeason}/${ref.idStage}/${ref.idMatch}?language=${apiLang(lang)}`
895
+ );
896
+ if (!Array.isArray(data?.Event))
897
+ throw new SchemaError("fifa", "timeline Event is not an array");
898
+ const map = fifa_event_map_default;
899
+ return data.Event.map((e, i) => {
900
+ const type = map[String(e.Type)] ?? "UNKNOWN";
901
+ const { minute, injury } = parseMatchMinute(e.MatchMinute);
902
+ const text = e.EventDescription?.[0]?.Description || void 0;
903
+ const teamSide = e.IdTeam && ref.idHomeTeam && String(e.IdTeam) === ref.idHomeTeam ? "home" : e.IdTeam && ref.idAwayTeam && String(e.IdTeam) === ref.idAwayTeam ? "away" : void 0;
904
+ const isGoal = type === "GOAL" || type === "OWN_GOAL" || type === "PENALTY_GOAL";
905
+ return {
906
+ id: `fifa:${e.EventId ?? `${e.Type}-${e.MatchMinute}-${i}`}`,
907
+ type,
908
+ minute,
909
+ ...injury !== void 0 ? { injury } : {},
910
+ period: e.Period,
911
+ ...e.IdTeam ? { teamCode: teamSide ? match[teamSide].code : String(e.IdTeam) } : {},
912
+ ...teamSide ? { teamSide } : {},
913
+ ...PLAYER_TYPES.has(type) ? { player: extractPlayer(text) } : {},
914
+ text: text ?? eventSentence({ type }, lang, {
915
+ team: teamSide ? match[teamSide].name : void 0
916
+ }),
917
+ ...isGoal && typeof e.HomeGoals === "number" ? { scoreAfter: { home: e.HomeGoals, away: e.AwayGoals } } : {},
918
+ source: "fifa",
919
+ seq: i
920
+ };
921
+ });
922
+ }
923
+ };
924
+
925
+ // src/core/winprob.ts
926
+ function impliedFromOdds(odds) {
927
+ const h = 1 / odds.home;
928
+ const d = 1 / odds.draw;
929
+ const a = 1 / odds.away;
930
+ const s = h + d + a;
931
+ return { home: h / s, draw: d / s, away: a / s };
932
+ }
933
+ function poissonPmf(k, lambda) {
934
+ let p = Math.exp(-lambda);
935
+ for (let i = 1; i <= k; i++) p = p * lambda / i;
936
+ return p;
937
+ }
938
+ var MAX_GOALS = 10;
939
+ function outcomeProbs(lh, la, diff = 0) {
940
+ let home = 0;
941
+ let draw = 0;
942
+ let away = 0;
943
+ for (let gh = 0; gh <= MAX_GOALS; gh++) {
944
+ const ph = poissonPmf(gh, lh);
945
+ for (let ga = 0; ga <= MAX_GOALS; ga++) {
946
+ const p = ph * poissonPmf(ga, la);
947
+ const final = diff + gh - ga;
948
+ if (final > 0) home += p;
949
+ else if (final < 0) away += p;
950
+ else draw += p;
951
+ }
952
+ }
953
+ const s = home + draw + away;
954
+ return { home: home / s, draw: draw / s, away: away / s };
955
+ }
956
+ function estimateLambdas(prior) {
957
+ let best = { lh: 1.3, la: 1.1 };
958
+ let bestErr = Infinity;
959
+ for (let lh = 0.3; lh <= 3.2; lh += 0.1) {
960
+ for (let la = 0.3; la <= 3.2; la += 0.1) {
961
+ const p = outcomeProbs(lh, la);
962
+ const err = (p.home - prior.home) ** 2 + (p.draw - prior.draw) ** 2 + (p.away - prior.away) ** 2;
963
+ if (err < bestErr) {
964
+ bestErr = err;
965
+ best = { lh, la };
966
+ }
967
+ }
968
+ }
969
+ return best;
970
+ }
971
+ var DEFAULT_LAMBDAS = { lh: 1.35, la: 1.15 };
972
+ function liveWinProb(opts) {
973
+ const { lh, la } = opts.lambdas ?? DEFAULT_LAMBDAS;
974
+ const remaining = Math.min(1, Math.max(0, 1 - opts.elapsedFraction));
975
+ const diff = opts.scoreHome - opts.scoreAway;
976
+ return outcomeProbs(lh * remaining, la * remaining, diff);
977
+ }
978
+
979
+ // src/data/odds.ts
980
+ function americanToDecimal(x) {
981
+ if (typeof x !== "number" || !Number.isFinite(x) || x === 0) return null;
982
+ return x > 0 ? 1 + x / 100 : 1 + 100 / Math.abs(x);
983
+ }
984
+ async function fetchPreMatchLambdas(match) {
985
+ const ref = match.sourceRefs.espn;
986
+ if (!ref) return null;
987
+ try {
988
+ const data = await getJson(
989
+ `${ESPN_BASE}/${ESPN_WC_SLUG}/summary?event=${ref.eventId}`
990
+ );
991
+ const candidates = [
992
+ ...Array.isArray(data?.pickcenter) ? data.pickcenter : [],
993
+ ...Array.isArray(data?.odds) ? data.odds : []
994
+ ];
995
+ for (const pc of candidates) {
996
+ const h = americanToDecimal(pc?.homeTeamOdds?.moneyLine);
997
+ const a = americanToDecimal(pc?.awayTeamOdds?.moneyLine);
998
+ const d = americanToDecimal(pc?.drawOdds?.moneyLine);
999
+ if (h && a && d) {
1000
+ const prior = impliedFromOdds({ home: h, draw: d, away: a });
1001
+ return estimateLambdas(prior);
1002
+ }
1003
+ }
1004
+ return null;
1005
+ } catch {
1006
+ return null;
1007
+ }
1008
+ }
1009
+
1010
+ // src/data/demo-match.json
1011
+ var demo_match_default = {
1012
+ match: {
1013
+ id: "400021529",
1014
+ stage: "16\uAC15\uC804",
1015
+ kickoff: "2026-07-06T19:00:00Z",
1016
+ home: {
1017
+ code: "POR",
1018
+ name: "\uD3EC\uB974\uD22C\uAC08",
1019
+ flag: "\u{1F1F5}\u{1F1F9}"
1020
+ },
1021
+ away: {
1022
+ code: "ESP",
1023
+ name: "\uC2A4\uD398\uC778",
1024
+ flag: "\u{1F1EA}\u{1F1F8}"
1025
+ },
1026
+ score: {
1027
+ home: 0,
1028
+ away: 0
1029
+ },
1030
+ phase: "FIRST_HALF",
1031
+ sourceRefs: {
1032
+ fifa: {
1033
+ idCompetition: "17",
1034
+ idSeason: "285023",
1035
+ idStage: "289288",
1036
+ idMatch: "400021529"
1037
+ }
1038
+ }
1039
+ },
1040
+ events: [
1041
+ {
1042
+ id: "fifa:1205161984",
1043
+ type: "COIN_TOSS",
1044
+ minute: 0,
1045
+ period: 2,
1046
+ teamSide: "away",
1047
+ teamCode: "ESP",
1048
+ text: "\uC2A4\uD398\uC778 \uD300\uC774 \uB3D9\uC804 \uB358\uC9C0\uAE30\uC5D0\uC11C \uC2B9\uB9AC\uD574 \uC120\uCD95\uC744 \uC120\uD0DD\uD588\uB2E4",
1049
+ source: "fifa",
1050
+ seq: 0
1051
+ },
1052
+ {
1053
+ id: "fifa:1325757440",
1054
+ type: "PERIOD_START",
1055
+ minute: 0,
1056
+ period: 3,
1057
+ text: "\uC8FC\uC2EC\uC774 \uC804\uBC18\uC804 \uC2DC\uC791 \uD718\uC2AC\uC744 \uBD88\uC5C8\uC2B5\uB2C8\uB2E4.",
1058
+ source: "fifa",
1059
+ seq: 1
1060
+ },
1061
+ {
1062
+ id: "fifa:1434305540",
1063
+ type: "FOUL",
1064
+ minute: 2,
1065
+ period: 3,
1066
+ teamSide: "away",
1067
+ teamCode: "ESP",
1068
+ player: "Lamine YAMAL",
1069
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1070
+ source: "fifa",
1071
+ seq: 2
1072
+ },
1073
+ {
1074
+ id: "fifa:1494946821",
1075
+ type: "SHOT",
1076
+ minute: 3,
1077
+ period: 3,
1078
+ teamSide: "away",
1079
+ teamCode: "ESP",
1080
+ player: "Mikel OYARZABAL",
1081
+ text: "Mikel OYARZABAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1082
+ source: "fifa",
1083
+ seq: 3
1084
+ },
1085
+ {
1086
+ id: "fifa:254022764",
1087
+ type: "SAVE",
1088
+ minute: 3,
1089
+ period: 3,
1090
+ teamSide: "home",
1091
+ teamCode: "POR",
1092
+ text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1093
+ source: "fifa",
1094
+ seq: 4
1095
+ },
1096
+ {
1097
+ id: "fifa:1724649474",
1098
+ type: "SHOT",
1099
+ minute: 7,
1100
+ period: 3,
1101
+ teamSide: "home",
1102
+ teamCode: "POR",
1103
+ player: "JOAO CANCELO",
1104
+ text: "JOAO CANCELO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1105
+ source: "fifa",
1106
+ seq: 5
1107
+ },
1108
+ {
1109
+ id: "fifa:1817323521",
1110
+ type: "SHOT",
1111
+ minute: 8,
1112
+ period: 3,
1113
+ teamSide: "away",
1114
+ teamCode: "ESP",
1115
+ player: "Mikel OYARZABAL",
1116
+ text: "Mikel OYARZABAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1117
+ source: "fifa",
1118
+ seq: 6
1119
+ },
1120
+ {
1121
+ id: "fifa:1910263816",
1122
+ type: "CORNER",
1123
+ minute: 10,
1124
+ period: 3,
1125
+ teamSide: "home",
1126
+ teamCode: "POR",
1127
+ text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1128
+ source: "fifa",
1129
+ seq: 7
1130
+ },
1131
+ {
1132
+ id: "fifa:1910787075",
1133
+ type: "SHOT",
1134
+ minute: 10,
1135
+ period: 3,
1136
+ teamSide: "home",
1137
+ teamCode: "POR",
1138
+ player: "PEDRO NETO",
1139
+ text: "PEDRO NETO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1140
+ source: "fifa",
1141
+ seq: 8
1142
+ },
1143
+ {
1144
+ id: "fifa:1986359306",
1145
+ type: "FOUL",
1146
+ minute: 11,
1147
+ period: 3,
1148
+ teamSide: "home",
1149
+ teamCode: "POR",
1150
+ player: "JOAO FELIX",
1151
+ text: "JOAO FELIX (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1152
+ source: "fifa",
1153
+ seq: 9
1154
+ },
1155
+ {
1156
+ id: "fifa:2013177867",
1157
+ type: "SHOT",
1158
+ minute: 12,
1159
+ period: 3,
1160
+ teamSide: "home",
1161
+ teamCode: "POR",
1162
+ player: "CRISTIANO RONALDO",
1163
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1164
+ source: "fifa",
1165
+ seq: 10
1166
+ },
1167
+ {
1168
+ id: "fifa:774430092",
1169
+ type: "SAVE",
1170
+ minute: 12,
1171
+ period: 3,
1172
+ teamSide: "away",
1173
+ teamCode: "ESP",
1174
+ text: "\uC2A4\uD398\uC778 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1175
+ source: "fifa",
1176
+ seq: 11
1177
+ },
1178
+ {
1179
+ id: "fifa:2059471875",
1180
+ type: "CORNER",
1181
+ minute: 12,
1182
+ period: 3,
1183
+ teamSide: "home",
1184
+ teamCode: "POR",
1185
+ text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1186
+ source: "fifa",
1187
+ seq: 12
1188
+ },
1189
+ {
1190
+ id: "fifa:2144601093",
1191
+ type: "SHOT",
1192
+ minute: 14,
1193
+ period: 3,
1194
+ teamSide: "away",
1195
+ teamCode: "ESP",
1196
+ player: "Alex BAENA",
1197
+ text: "Alex BAENA (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1198
+ source: "fifa",
1199
+ seq: 13
1200
+ },
1201
+ {
1202
+ id: "fifa:138480647",
1203
+ type: "SHOT",
1204
+ minute: 16,
1205
+ period: 3,
1206
+ teamSide: "away",
1207
+ teamCode: "ESP",
1208
+ player: "Lamine YAMAL",
1209
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1210
+ source: "fifa",
1211
+ seq: 14
1212
+ },
1213
+ {
1214
+ id: "fifa:1047824932",
1215
+ type: "SAVE",
1216
+ minute: 16,
1217
+ period: 3,
1218
+ teamSide: "home",
1219
+ teamCode: "POR",
1220
+ text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1221
+ source: "fifa",
1222
+ seq: 15
1223
+ },
1224
+ {
1225
+ id: "fifa:141809670",
1226
+ type: "SHOT",
1227
+ minute: 16,
1228
+ period: 3,
1229
+ teamSide: "away",
1230
+ teamCode: "ESP",
1231
+ player: "Alex BAENA",
1232
+ text: "Alex BAENA (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1233
+ source: "fifa",
1234
+ seq: 16
1235
+ },
1236
+ {
1237
+ id: "fifa:1049966151",
1238
+ type: "SAVE",
1239
+ minute: 16,
1240
+ period: 3,
1241
+ teamSide: "home",
1242
+ teamCode: "POR",
1243
+ text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1244
+ source: "fifa",
1245
+ seq: 17
1246
+ },
1247
+ {
1248
+ id: "fifa:196097033",
1249
+ type: "CORNER",
1250
+ minute: 17,
1251
+ period: 3,
1252
+ teamSide: "away",
1253
+ teamCode: "ESP",
1254
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1255
+ source: "fifa",
1256
+ seq: 18
1257
+ },
1258
+ {
1259
+ id: "fifa:307362826",
1260
+ type: "FOUL",
1261
+ minute: 19,
1262
+ period: 3,
1263
+ teamSide: "away",
1264
+ teamCode: "ESP",
1265
+ player: "Alex BAENA",
1266
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1267
+ source: "fifa",
1268
+ seq: 19
1269
+ },
1270
+ {
1271
+ id: "fifa:432615434",
1272
+ type: "OFFSIDE",
1273
+ minute: 21,
1274
+ period: 3,
1275
+ teamSide: "home",
1276
+ teamCode: "POR",
1277
+ player: "BRUNO FERNANDES",
1278
+ text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218, \uC624\uD504\uC0AC\uC774\uB4DC\uB85C \uD310\uC815\uB429\uB2C8\uB2E4.",
1279
+ source: "fifa",
1280
+ seq: 20
1281
+ },
1282
+ {
1283
+ id: "fifa:502609932",
1284
+ type: "FOUL",
1285
+ minute: 22,
1286
+ period: 3,
1287
+ teamSide: "home",
1288
+ teamCode: "POR",
1289
+ player: "JOAO CANCELO",
1290
+ text: "JOAO CANCELO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1291
+ source: "fifa",
1292
+ seq: 21
1293
+ },
1294
+ {
1295
+ id: "fifa:554061837",
1296
+ type: "SHOT",
1297
+ minute: 23,
1298
+ period: 3,
1299
+ teamSide: "away",
1300
+ teamCode: "ESP",
1301
+ player: "Pedro PORRO",
1302
+ text: "Pedro PORRO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1303
+ source: "fifa",
1304
+ seq: 22
1305
+ },
1306
+ {
1307
+ id: "fifa:564411406",
1308
+ type: "BREAK",
1309
+ minute: 23,
1310
+ period: 3,
1311
+ text: "\uC218\uBD84 \uBCF4\uCDA9\uC744 \uC704\uD55C \uD734\uC2DD\uC73C\uB85C \uACBD\uAE30 \uC911\uB2E8",
1312
+ source: "fifa",
1313
+ seq: 23
1314
+ },
1315
+ {
1316
+ id: "fifa:771050509",
1317
+ type: "RESUMED",
1318
+ minute: 26,
1319
+ period: 3,
1320
+ text: "\uC911\uB2E8 \uD6C4 \uACBD\uAE30 \uC7AC\uAC1C",
1321
+ source: "fifa",
1322
+ seq: 24
1323
+ },
1324
+ {
1325
+ id: "fifa:965458958",
1326
+ type: "FOUL",
1327
+ minute: 29,
1328
+ period: 3,
1329
+ teamSide: "home",
1330
+ teamCode: "POR",
1331
+ player: "PEDRO NETO",
1332
+ text: "PEDRO NETO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1333
+ source: "fifa",
1334
+ seq: 25
1335
+ },
1336
+ {
1337
+ id: "fifa:1012404241",
1338
+ type: "CORNER",
1339
+ minute: 30,
1340
+ period: 3,
1341
+ teamSide: "away",
1342
+ teamCode: "ESP",
1343
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1344
+ source: "fifa",
1345
+ seq: 26
1346
+ },
1347
+ {
1348
+ id: "fifa:1027962898",
1349
+ type: "SHOT",
1350
+ minute: 31,
1351
+ period: 3,
1352
+ teamSide: "away",
1353
+ teamCode: "ESP",
1354
+ player: "Dani OLMO",
1355
+ text: "Dani OLMO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1356
+ source: "fifa",
1357
+ seq: 27
1358
+ },
1359
+ {
1360
+ id: "fifa:1070538769",
1361
+ type: "FOUL",
1362
+ minute: 31,
1363
+ period: 3,
1364
+ teamSide: "away",
1365
+ teamCode: "ESP",
1366
+ player: "Aymeric LAPORTE",
1367
+ text: "Aymeric LAPORTE (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1368
+ source: "fifa",
1369
+ seq: 28
1370
+ },
1371
+ {
1372
+ id: "fifa:1278368787",
1373
+ type: "FOUL",
1374
+ minute: 35,
1375
+ period: 3,
1376
+ teamSide: "away",
1377
+ teamCode: "ESP",
1378
+ player: "Lamine YAMAL",
1379
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1380
+ source: "fifa",
1381
+ seq: 29
1382
+ },
1383
+ {
1384
+ id: "fifa:1427833876",
1385
+ type: "SHOT",
1386
+ minute: 37,
1387
+ period: 3,
1388
+ teamSide: "home",
1389
+ teamCode: "POR",
1390
+ player: "CRISTIANO RONALDO",
1391
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1392
+ source: "fifa",
1393
+ seq: 30
1394
+ },
1395
+ {
1396
+ id: "fifa:189306026",
1397
+ type: "SAVE",
1398
+ minute: 37,
1399
+ period: 3,
1400
+ teamSide: "away",
1401
+ teamCode: "ESP",
1402
+ text: "\uC2A4\uD398\uC778 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1403
+ source: "fifa",
1404
+ seq: 31
1405
+ },
1406
+ {
1407
+ id: "fifa:1584167954",
1408
+ type: "FOUL",
1409
+ minute: 39,
1410
+ period: 3,
1411
+ teamSide: "away",
1412
+ teamCode: "ESP",
1413
+ player: "Pedro PORRO",
1414
+ text: "Pedro PORRO (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1415
+ source: "fifa",
1416
+ seq: 32
1417
+ },
1418
+ {
1419
+ id: "fifa:1679452179",
1420
+ type: "CORNER",
1421
+ minute: 41,
1422
+ period: 3,
1423
+ teamSide: "home",
1424
+ teamCode: "POR",
1425
+ text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1426
+ source: "fifa",
1427
+ seq: 33
1428
+ },
1429
+ {
1430
+ id: "fifa:1685583894",
1431
+ type: "SHOT",
1432
+ minute: 41,
1433
+ period: 3,
1434
+ teamSide: "home",
1435
+ teamCode: "POR",
1436
+ player: "NUNO MENDES",
1437
+ text: "NUNO MENDES (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1438
+ source: "fifa",
1439
+ seq: 34
1440
+ },
1441
+ {
1442
+ id: "fifa:1941175316",
1443
+ type: "SHOT",
1444
+ minute: 45,
1445
+ period: 3,
1446
+ teamSide: "away",
1447
+ teamCode: "ESP",
1448
+ player: "RODRI",
1449
+ text: "RODRI (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1450
+ source: "fifa",
1451
+ seq: 35
1452
+ },
1453
+ {
1454
+ id: "fifa:2046811160",
1455
+ type: "OFFSIDE",
1456
+ minute: 45,
1457
+ injury: 2,
1458
+ period: 3,
1459
+ teamSide: "away",
1460
+ teamCode: "ESP",
1461
+ player: "Lamine YAMAL",
1462
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218, \uC624\uD504\uC0AC\uC774\uB4DC\uB85C \uD310\uC815\uB429\uB2C8\uB2E4.",
1463
+ source: "fifa",
1464
+ seq: 36
1465
+ },
1466
+ {
1467
+ id: "fifa:2094483480",
1468
+ type: "FOUL",
1469
+ minute: 45,
1470
+ injury: 2,
1471
+ period: 3,
1472
+ teamSide: "home",
1473
+ teamCode: "POR",
1474
+ player: "RUBEN DIAS",
1475
+ text: "RUBEN DIAS (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1476
+ source: "fifa",
1477
+ seq: 37
1478
+ },
1479
+ {
1480
+ id: "fifa:23969820",
1481
+ type: "FOUL",
1482
+ minute: 45,
1483
+ injury: 3,
1484
+ period: 3,
1485
+ teamSide: "away",
1486
+ teamCode: "ESP",
1487
+ player: "Pau CUBARSI",
1488
+ text: "Pau CUBARSI (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1489
+ source: "fifa",
1490
+ seq: 38
1491
+ },
1492
+ {
1493
+ id: "fifa:121538587",
1494
+ type: "FOUL",
1495
+ minute: 45,
1496
+ injury: 6,
1497
+ period: 3,
1498
+ teamSide: "home",
1499
+ teamCode: "POR",
1500
+ player: "JOAO FELIX",
1501
+ text: "JOAO FELIX (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1502
+ source: "fifa",
1503
+ seq: 39
1504
+ },
1505
+ {
1506
+ id: "fifa:170851354",
1507
+ type: "PERIOD_END",
1508
+ minute: 45,
1509
+ injury: 7,
1510
+ period: 3,
1511
+ text: "\uC8FC\uC2EC\uC774 \uC804\uBC18\uC804 \uC885\uB8CC\uB97C \uC120\uC5B8\uD569\uB2C8\uB2E4.",
1512
+ source: "fifa",
1513
+ seq: 40
1514
+ },
1515
+ {
1516
+ id: "fifa:1182601245",
1517
+ type: "PERIOD_START",
1518
+ minute: 45,
1519
+ period: 5,
1520
+ text: "\uC8FC\uC2EC\uC774 \uD6C4\uBC18\uC804 \uC2DC\uC791 \uD718\uC2AC\uC744 \uBD88\uC5C8\uC2B5\uB2C8\uB2E4.",
1521
+ source: "fifa",
1522
+ seq: 41
1523
+ },
1524
+ {
1525
+ id: "fifa:1342339100",
1526
+ type: "FOUL",
1527
+ minute: 48,
1528
+ period: 5,
1529
+ teamSide: "away",
1530
+ teamCode: "ESP",
1531
+ player: "Dani OLMO",
1532
+ text: "Dani OLMO (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1533
+ source: "fifa",
1534
+ seq: 42
1535
+ },
1536
+ {
1537
+ id: "fifa:1467330591",
1538
+ type: "FOUL",
1539
+ minute: 50,
1540
+ period: 5,
1541
+ teamSide: "away",
1542
+ teamCode: "ESP",
1543
+ player: "Lamine YAMAL",
1544
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1545
+ source: "fifa",
1546
+ seq: 43
1547
+ },
1548
+ {
1549
+ id: "fifa:1816513568",
1550
+ type: "SUBSTITUTION",
1551
+ minute: 56,
1552
+ period: 5,
1553
+ teamSide: "home",
1554
+ teamCode: "POR",
1555
+ text: "NELSON SEMEDO (in) \uC120\uC218\uAC00 NUNO MENDES(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1556
+ source: "fifa",
1557
+ seq: 44
1558
+ },
1559
+ {
1560
+ id: "fifa:1934637083",
1561
+ type: "OFFSIDE",
1562
+ minute: 58,
1563
+ period: 5,
1564
+ teamSide: "home",
1565
+ teamCode: "POR",
1566
+ player: "CRISTIANO RONALDO",
1567
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218, \uC624\uD504\uC0AC\uC774\uB4DC\uB85C \uD310\uC815\uB429\uB2C8\uB2E4.",
1568
+ source: "fifa",
1569
+ seq: 45
1570
+ },
1571
+ {
1572
+ id: "fifa:2020733986",
1573
+ type: "SHOT",
1574
+ minute: 59,
1575
+ period: 5,
1576
+ teamSide: "home",
1577
+ teamCode: "POR",
1578
+ player: "CRISTIANO RONALDO",
1579
+ text: "CRISTIANO RONALDO (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1580
+ source: "fifa",
1581
+ seq: 46
1582
+ },
1583
+ {
1584
+ id: "fifa:2122111005",
1585
+ type: "FOUL",
1586
+ minute: 61,
1587
+ period: 5,
1588
+ teamSide: "away",
1589
+ teamCode: "ESP",
1590
+ player: "Lamine YAMAL",
1591
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1592
+ source: "fifa",
1593
+ seq: 47
1594
+ },
1595
+ {
1596
+ id: "fifa:2141721635",
1597
+ type: "SHOT",
1598
+ minute: 61,
1599
+ period: 5,
1600
+ teamSide: "away",
1601
+ teamCode: "ESP",
1602
+ player: "PEDRI",
1603
+ text: "PEDRI (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1604
+ source: "fifa",
1605
+ seq: 48
1606
+ },
1607
+ {
1608
+ id: "fifa:25384996",
1609
+ type: "CORNER",
1610
+ minute: 62,
1611
+ period: 5,
1612
+ teamSide: "away",
1613
+ teamCode: "ESP",
1614
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1615
+ source: "fifa",
1616
+ seq: 49
1617
+ },
1618
+ {
1619
+ id: "fifa:33345565",
1620
+ type: "SHOT",
1621
+ minute: 62,
1622
+ period: 5,
1623
+ teamSide: "away",
1624
+ teamCode: "ESP",
1625
+ player: "Lamine YAMAL",
1626
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1627
+ source: "fifa",
1628
+ seq: 50
1629
+ },
1630
+ {
1631
+ id: "fifa:60943390",
1632
+ type: "CORNER",
1633
+ minute: 62,
1634
+ period: 5,
1635
+ teamSide: "away",
1636
+ teamCode: "ESP",
1637
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1638
+ source: "fifa",
1639
+ seq: 51
1640
+ },
1641
+ {
1642
+ id: "fifa:130297896",
1643
+ type: "FOUL",
1644
+ minute: 63,
1645
+ period: 5,
1646
+ teamSide: "away",
1647
+ teamCode: "ESP",
1648
+ player: "PEDRI",
1649
+ text: "PEDRI (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1650
+ source: "fifa",
1651
+ seq: 52
1652
+ },
1653
+ {
1654
+ id: "fifa:251141156",
1655
+ type: "SHOT",
1656
+ minute: 65,
1657
+ period: 5,
1658
+ teamSide: "away",
1659
+ teamCode: "ESP",
1660
+ player: "Alex BAENA",
1661
+ text: "Alex BAENA (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1662
+ source: "fifa",
1663
+ seq: 53
1664
+ },
1665
+ {
1666
+ id: "fifa:1165322336",
1667
+ type: "SAVE",
1668
+ minute: 65,
1669
+ period: 5,
1670
+ teamSide: "home",
1671
+ teamCode: "POR",
1672
+ text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1673
+ source: "fifa",
1674
+ seq: 54
1675
+ },
1676
+ {
1677
+ id: "fifa:423070762",
1678
+ type: "BREAK",
1679
+ minute: 68,
1680
+ period: 5,
1681
+ text: "\uC218\uBD84 \uBCF4\uCDA9\uC744 \uC704\uD55C \uD734\uC2DD\uC73C\uB85C \uACBD\uAE30 \uC911\uB2E8",
1682
+ source: "fifa",
1683
+ seq: 55
1684
+ },
1685
+ {
1686
+ id: "fifa:618886182",
1687
+ type: "SUBSTITUTION",
1688
+ minute: 71,
1689
+ period: 5,
1690
+ teamSide: "home",
1691
+ teamCode: "POR",
1692
+ text: "RAFAEL LEAO (in) \uC120\uC218\uAC00 JOAO FELIX(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1693
+ source: "fifa",
1694
+ seq: 56
1695
+ },
1696
+ {
1697
+ id: "fifa:618886183",
1698
+ type: "SUBSTITUTION",
1699
+ minute: 71,
1700
+ period: 5,
1701
+ teamSide: "home",
1702
+ teamCode: "POR",
1703
+ text: "DIOGO DALOT (in) \uC120\uC218\uAC00 JOAO CANCELO(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1704
+ source: "fifa",
1705
+ seq: 57
1706
+ },
1707
+ {
1708
+ id: "fifa:644447262",
1709
+ type: "RESUMED",
1710
+ minute: 72,
1711
+ period: 5,
1712
+ text: "\uC911\uB2E8 \uD6C4 \uACBD\uAE30 \uC7AC\uAC1C",
1713
+ source: "fifa",
1714
+ seq: 58
1715
+ },
1716
+ {
1717
+ id: "fifa:670940201",
1718
+ type: "FOUL",
1719
+ minute: 72,
1720
+ period: 5,
1721
+ teamSide: "home",
1722
+ teamCode: "POR",
1723
+ player: "NELSON SEMEDO",
1724
+ text: "NELSON SEMEDO (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1725
+ source: "fifa",
1726
+ seq: 59
1727
+ },
1728
+ {
1729
+ id: "fifa:726086688",
1730
+ type: "SHOT",
1731
+ minute: 73,
1732
+ period: 5,
1733
+ teamSide: "away",
1734
+ teamCode: "ESP",
1735
+ player: "Lamine YAMAL",
1736
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1737
+ source: "fifa",
1738
+ seq: 60
1739
+ },
1740
+ {
1741
+ id: "fifa:1631833456",
1742
+ type: "SAVE",
1743
+ minute: 73,
1744
+ period: 5,
1745
+ teamSide: "home",
1746
+ teamCode: "POR",
1747
+ text: "\uD3EC\uB974\uD22C\uAC08 \uC758 \uACE8\uD0A4\uD37C\uAC00 \uC120\uBC29\uC744 \uAE30\uB85D\uD569\uB2C8\uB2E4.",
1748
+ source: "fifa",
1749
+ seq: 61
1750
+ },
1751
+ {
1752
+ id: "fifa:750732331",
1753
+ type: "CORNER",
1754
+ minute: 73,
1755
+ period: 5,
1756
+ teamSide: "away",
1757
+ teamCode: "ESP",
1758
+ text: "Alex BAENA (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1759
+ source: "fifa",
1760
+ seq: 62
1761
+ },
1762
+ {
1763
+ id: "fifa:872663074",
1764
+ type: "SUBSTITUTION",
1765
+ minute: 75,
1766
+ period: 5,
1767
+ teamSide: "away",
1768
+ teamCode: "ESP",
1769
+ text: "Ferran TORRES (in) \uC120\uC218\uAC00 Alex BAENA(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1770
+ source: "fifa",
1771
+ seq: 63
1772
+ },
1773
+ {
1774
+ id: "fifa:892006443",
1775
+ type: "SHOT",
1776
+ minute: 76,
1777
+ period: 5,
1778
+ teamSide: "home",
1779
+ teamCode: "POR",
1780
+ player: "VITINHA",
1781
+ text: "VITINHA (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1782
+ source: "fifa",
1783
+ seq: 64
1784
+ },
1785
+ {
1786
+ id: "fifa:895588396",
1787
+ type: "SHOT",
1788
+ minute: 76,
1789
+ period: 5,
1790
+ teamSide: "home",
1791
+ teamCode: "POR",
1792
+ player: "BRUNO FERNANDES",
1793
+ text: "BRUNO FERNANDES (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1794
+ source: "fifa",
1795
+ seq: 65
1796
+ },
1797
+ {
1798
+ id: "fifa:991821855",
1799
+ type: "SHOT",
1800
+ minute: 77,
1801
+ period: 5,
1802
+ teamSide: "away",
1803
+ teamCode: "ESP",
1804
+ player: "Ferran TORRES",
1805
+ text: "Ferran TORRES (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1806
+ source: "fifa",
1807
+ seq: 66
1808
+ },
1809
+ {
1810
+ id: "fifa:1057817637",
1811
+ type: "SHOT",
1812
+ minute: 78,
1813
+ period: 5,
1814
+ teamSide: "away",
1815
+ teamCode: "ESP",
1816
+ player: "Dani OLMO",
1817
+ text: "Dani OLMO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1818
+ source: "fifa",
1819
+ seq: 67
1820
+ },
1821
+ {
1822
+ id: "fifa:1091701793",
1823
+ type: "CORNER",
1824
+ minute: 79,
1825
+ period: 5,
1826
+ teamSide: "away",
1827
+ teamCode: "ESP",
1828
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1829
+ source: "fifa",
1830
+ seq: 68
1831
+ },
1832
+ {
1833
+ id: "fifa:1160306726",
1834
+ type: "CORNER",
1835
+ minute: 80,
1836
+ period: 5,
1837
+ teamSide: "away",
1838
+ teamCode: "ESP",
1839
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uCF54\uB108\uD0A5\uC744 \uCC39\uB2C8\uB2E4.",
1840
+ source: "fifa",
1841
+ seq: 69
1842
+ },
1843
+ {
1844
+ id: "fifa:1169144866",
1845
+ type: "FOUL",
1846
+ minute: 80,
1847
+ period: 5,
1848
+ teamSide: "away",
1849
+ teamCode: "ESP",
1850
+ player: "RODRI",
1851
+ text: "RODRI (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1852
+ source: "fifa",
1853
+ seq: 70
1854
+ },
1855
+ {
1856
+ id: "fifa:1341277231",
1857
+ type: "SUBSTITUTION",
1858
+ minute: 83,
1859
+ period: 5,
1860
+ teamSide: "home",
1861
+ teamCode: "POR",
1862
+ text: "BERNARDO SILVA (in) \uC120\uC218\uAC00 VITINHA(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1863
+ source: "fifa",
1864
+ seq: 71
1865
+ },
1866
+ {
1867
+ id: "fifa:1341277230",
1868
+ type: "SUBSTITUTION",
1869
+ minute: 83,
1870
+ period: 5,
1871
+ teamSide: "home",
1872
+ teamCode: "POR",
1873
+ text: "FRANCISCO CONCEICAO (in) \uC120\uC218\uAC00 PEDRO NETO(\uAD50\uCCB4)(\uD3EC\uB974\uD22C\uAC08) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1874
+ source: "fifa",
1875
+ seq: 72
1876
+ },
1877
+ {
1878
+ id: "fifa:1501911078",
1879
+ type: "FOUL",
1880
+ minute: 83,
1881
+ period: 5,
1882
+ teamSide: "away",
1883
+ teamCode: "ESP",
1884
+ player: "Lamine YAMAL",
1885
+ text: "Lamine YAMAL (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1886
+ source: "fifa",
1887
+ seq: 73
1888
+ },
1889
+ {
1890
+ id: "fifa:1469545514",
1891
+ type: "SUBSTITUTION",
1892
+ minute: 85,
1893
+ period: 5,
1894
+ teamSide: "away",
1895
+ teamCode: "ESP",
1896
+ text: "Mikel MERINO (in) \uC120\uC218\uAC00 Dani OLMO(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1897
+ source: "fifa",
1898
+ seq: 74
1899
+ },
1900
+ {
1901
+ id: "fifa:1469545515",
1902
+ type: "SUBSTITUTION",
1903
+ minute: 85,
1904
+ period: 5,
1905
+ teamSide: "away",
1906
+ teamCode: "ESP",
1907
+ text: "Fabian RUIZ (in) \uC120\uC218\uAC00 PEDRI(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
1908
+ source: "fifa",
1909
+ seq: 75
1910
+ },
1911
+ {
1912
+ id: "fifa:1695860784",
1913
+ type: "YELLOW",
1914
+ minute: 89,
1915
+ period: 5,
1916
+ teamSide: "home",
1917
+ teamCode: "POR",
1918
+ player: "\uC8FC\uC2EC\uC774 BERNARDO SILVA",
1919
+ text: "\uC8FC\uC2EC\uC774 BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uC5D0\uAC8C \uACBD\uACE0 \uC870\uCE58\uB97C \uD569\uB2C8\uB2E4.",
1920
+ source: "fifa",
1921
+ seq: 76
1922
+ },
1923
+ {
1924
+ id: "fifa:1806117938",
1925
+ type: "FOUL",
1926
+ minute: 90,
1927
+ period: 5,
1928
+ teamSide: "home",
1929
+ teamCode: "POR",
1930
+ player: "BERNARDO SILVA",
1931
+ text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08) \uC120\uC218\uAC00 \uD30C\uC6B8\uC744 \uBC94\uD588\uC2B5\uB2C8\uB2E4.",
1932
+ source: "fifa",
1933
+ seq: 77
1934
+ },
1935
+ {
1936
+ id: "fifa:1835130926",
1937
+ type: "ASSIST",
1938
+ minute: 90,
1939
+ injury: 1,
1940
+ period: 5,
1941
+ teamSide: "away",
1942
+ teamCode: "ESP",
1943
+ text: "Ferran TORRES \uC120\uC218\uC758 \uC5B4\uC2DC\uC2A4\uD2B8\uC785\uB2C8\uB2E4.",
1944
+ source: "fifa",
1945
+ seq: 78
1946
+ },
1947
+ {
1948
+ id: "fifa:1816196148",
1949
+ type: "SHOT",
1950
+ minute: 90,
1951
+ injury: 1,
1952
+ period: 5,
1953
+ teamSide: "away",
1954
+ teamCode: "ESP",
1955
+ player: "Mikel MERINO",
1956
+ text: "Mikel MERINO (\uC2A4\uD398\uC778), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
1957
+ source: "fifa",
1958
+ seq: 79
1959
+ },
1960
+ {
1961
+ id: "fifa:1816196150",
1962
+ type: "GOAL",
1963
+ minute: 90,
1964
+ injury: 1,
1965
+ period: 5,
1966
+ teamSide: "away",
1967
+ teamCode: "ESP",
1968
+ player: "Mikel MERINO",
1969
+ text: "Mikel MERINO (\uC2A4\uD398\uC778) \uC120\uC218\uAC00 \uB4DD\uC810\uD569\uB2C8\uB2E4!!",
1970
+ scoreAfter: {
1971
+ home: 0,
1972
+ away: 1
1973
+ },
1974
+ source: "fifa",
1975
+ seq: 80
1976
+ },
1977
+ {
1978
+ id: "fifa:1997970488",
1979
+ type: "YELLOW",
1980
+ minute: 90,
1981
+ injury: 3,
1982
+ period: 5,
1983
+ teamSide: "home",
1984
+ teamCode: "POR",
1985
+ player: "\uC8FC\uC2EC\uC774 RENATO VEIGA",
1986
+ text: "\uC8FC\uC2EC\uC774 RENATO VEIGA (\uD3EC\uB974\uD22C\uAC08) \uC5D0\uAC8C \uACBD\uACE0 \uC870\uCE58\uB97C \uD569\uB2C8\uB2E4.",
1987
+ source: "fifa",
1988
+ seq: 81
1989
+ },
1990
+ {
1991
+ id: "fifa:22581296",
1992
+ type: "SHOT",
1993
+ minute: 90,
1994
+ injury: 6,
1995
+ period: 5,
1996
+ teamSide: "home",
1997
+ teamCode: "POR",
1998
+ player: "BERNARDO SILVA",
1999
+ text: "BERNARDO SILVA (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
2000
+ source: "fifa",
2001
+ seq: 82
2002
+ },
2003
+ {
2004
+ id: "fifa:74932281",
2005
+ type: "SUBSTITUTION",
2006
+ minute: 90,
2007
+ injury: 7,
2008
+ period: 5,
2009
+ teamSide: "away",
2010
+ teamCode: "ESP",
2011
+ text: "Borja IGLESIAS (in) \uC120\uC218\uAC00 Mikel OYARZABAL(\uAD50\uCCB4)(\uC2A4\uD398\uC778) \uC120\uC218 \uB300\uC2E0 \uAD50\uCCB4\uB418\uC5B4 \uACBD\uAE30\uC7A5\uC5D0 \uB4E4\uC5B4\uAC11\uB2C8\uB2E4.",
2012
+ source: "fifa",
2013
+ seq: 83
2014
+ },
2015
+ {
2016
+ id: "fifa:153596981",
2017
+ type: "YELLOW",
2018
+ minute: 90,
2019
+ injury: 8,
2020
+ period: 5,
2021
+ teamSide: "away",
2022
+ teamCode: "ESP",
2023
+ player: "\uC8FC\uC2EC\uC774 Ferran TORRES",
2024
+ text: "\uC8FC\uC2EC\uC774 Ferran TORRES (\uC2A4\uD398\uC778) \uC5D0\uAC8C \uACBD\uACE0 \uC870\uCE58\uB97C \uD569\uB2C8\uB2E4.",
2025
+ source: "fifa",
2026
+ seq: 84
2027
+ },
2028
+ {
2029
+ id: "fifa:184755258",
2030
+ type: "SHOT",
2031
+ minute: 90,
2032
+ injury: 9,
2033
+ period: 5,
2034
+ teamSide: "home",
2035
+ teamCode: "POR",
2036
+ player: "JOAO NEVES",
2037
+ text: "JOAO NEVES (\uD3EC\uB974\uD22C\uAC08), \uACE8\uBB38\uC744 \uD5A5\uD574 \uC288\uD305\uC744 \uC2DC\uB3C4\uD569\uB2C8\uB2E4.",
2038
+ source: "fifa",
2039
+ seq: 85
2040
+ },
2041
+ {
2042
+ id: "fifa:193863738",
2043
+ type: "PERIOD_END",
2044
+ minute: 90,
2045
+ injury: 9,
2046
+ period: 5,
2047
+ text: "\uC8FC\uC2EC\uC774 \uD6C4\uBC18\uC804 \uC885\uB8CC\uB97C \uC120\uC5B8\uD569\uB2C8\uB2E4.",
2048
+ source: "fifa",
2049
+ seq: 86
2050
+ },
2051
+ {
2052
+ id: "fifa:230253627",
2053
+ type: "FULLTIME",
2054
+ minute: 99,
2055
+ period: 10,
2056
+ text: "\uACBD\uAE30 \uC885\uB8CC \uD718\uC2AC\uC774 \uC6B8\uB9BD\uB2C8\uB2E4.",
2057
+ source: "fifa",
2058
+ seq: 87
2059
+ }
2060
+ ]
2061
+ };
2062
+
2063
+ // src/engine/polling.ts
2064
+ import { EventEmitter } from "events";
2065
+
2066
+ // src/core/diff.ts
2067
+ var EventDiffer = class {
2068
+ seen = /* @__PURE__ */ new Set();
2069
+ emittedHigh = [];
2070
+ currentSource = null;
2071
+ lastEmittedMinute = 0;
2072
+ reconcile(snapshot) {
2073
+ const source = snapshot[0]?.source ?? this.currentSource;
2074
+ const sourceSwitched = this.currentSource !== null && source !== null && source !== this.currentSource;
2075
+ if (sourceSwitched) {
2076
+ this.seen = /* @__PURE__ */ new Set();
2077
+ }
2078
+ const snapshotIds = new Set(snapshot.map((e) => e.id));
2079
+ const cancelled = [];
2080
+ if (!sourceSwitched) {
2081
+ for (const prev of this.emittedHigh) {
2082
+ if (prev.source === source && this.isCancellable(prev) && !snapshotIds.has(prev.id)) {
2083
+ cancelled.push(prev);
2084
+ }
2085
+ }
2086
+ if (cancelled.length > 0) {
2087
+ const gone = new Set(cancelled.map((e) => e.id));
2088
+ this.emittedHigh = this.emittedHigh.filter((e) => !gone.has(e.id));
2089
+ }
2090
+ }
2091
+ let fresh = [];
2092
+ for (const e of snapshot) {
2093
+ if (this.seen.has(e.id)) continue;
2094
+ this.seen.add(e.id);
2095
+ if (sourceSwitched) {
2096
+ if (salienceOf(e.type) === "high") {
2097
+ if (this.matchesEmittedHigh(e)) continue;
2098
+ } else if (e.minute < this.lastEmittedMinute) {
2099
+ continue;
2100
+ }
2101
+ }
2102
+ fresh.push(e);
2103
+ }
2104
+ fresh = fresh.sort(compareEvents);
2105
+ for (const e of fresh) {
2106
+ if (salienceOf(e.type) === "high") this.emittedHigh.push(e);
2107
+ if (e.minute > this.lastEmittedMinute) this.lastEmittedMinute = e.minute;
2108
+ }
2109
+ if (source) this.currentSource = source;
2110
+ return { fresh, cancelled, sourceSwitched };
2111
+ }
2112
+ isCancellable(e) {
2113
+ return e.type === "GOAL" || e.type === "OWN_GOAL" || e.type === "PENALTY_GOAL";
2114
+ }
2115
+ matchesEmittedHigh(e) {
2116
+ return this.emittedHigh.some(
2117
+ (p) => p.type === e.type && (p.period === void 0 || e.period === void 0 || p.period === e.period) && Math.abs(p.minute - e.minute) <= 1 && (p.teamCode === void 0 || e.teamCode === void 0 || p.teamCode === e.teamCode)
2118
+ );
2119
+ }
2120
+ };
2121
+
2122
+ // src/engine/polling.ts
2123
+ var PollingEngine = class extends EventEmitter {
2124
+ constructor(provider, match, lang, opts = {}) {
2125
+ super();
2126
+ this.provider = provider;
2127
+ this.match = match;
2128
+ this.lang = lang;
2129
+ this.intervalMs = opts.intervalMs ?? 1e4;
2130
+ this.jitterMs = opts.jitterMs ?? 2e3;
2131
+ }
2132
+ provider;
2133
+ match;
2134
+ lang;
2135
+ differ = new EventDiffer();
2136
+ intervalMs;
2137
+ jitterMs;
2138
+ stopped = false;
2139
+ timer = null;
2140
+ errorStreak = 0;
2141
+ lastOkAt;
2142
+ finishedPolls = 0;
2143
+ start() {
2144
+ void this.tick();
2145
+ }
2146
+ stop() {
2147
+ this.stopped = true;
2148
+ if (this.timer) clearTimeout(this.timer);
2149
+ }
2150
+ schedule(delayMs) {
2151
+ if (this.stopped) return;
2152
+ this.timer = setTimeout(() => void this.tick(), delayMs);
2153
+ }
2154
+ async tick() {
2155
+ if (this.stopped) return;
2156
+ try {
2157
+ const [state, timeline] = await Promise.all([
2158
+ this.provider.fetchMatchState(this.match, this.lang),
2159
+ this.provider.fetchTimeline(this.match, this.lang)
2160
+ ]);
2161
+ if (this.errorStreak > 0) {
2162
+ this.emit("net", { down: false, lastOkAt: Date.now() });
2163
+ }
2164
+ this.errorStreak = 0;
2165
+ this.lastOkAt = Date.now();
2166
+ if (state) {
2167
+ this.match.score = state.score;
2168
+ this.match.phase = state.phase;
2169
+ this.emit("state", state);
2170
+ }
2171
+ const { fresh, cancelled, sourceSwitched } = this.differ.reconcile(timeline);
2172
+ if (sourceSwitched) this.emit("sourceSwitched");
2173
+ if (fresh.length > 0) {
2174
+ this.emit("events", fresh);
2175
+ for (const e of fresh) {
2176
+ if (CELEBRATION_TYPES.has(e.type)) this.emit("goal", e);
2177
+ }
2178
+ }
2179
+ for (const c of cancelled) this.emit("cancelled", c);
2180
+ if (state?.phase === "FINISHED" || state?.phase === "ABANDONED") {
2181
+ if (++this.finishedPolls >= 2) {
2182
+ this.emit("finished");
2183
+ this.stop();
2184
+ return;
2185
+ }
2186
+ }
2187
+ this.schedule(this.intervalMs + Math.random() * this.jitterMs);
2188
+ } catch {
2189
+ this.errorStreak++;
2190
+ const backoff = Math.min(
2191
+ this.intervalMs * 2 ** Math.min(this.errorStreak - 1, 3),
2192
+ 6e4
2193
+ );
2194
+ this.emit("net", {
2195
+ down: true,
2196
+ retryInMs: backoff,
2197
+ lastOkAt: this.lastOkAt
2198
+ });
2199
+ this.schedule(backoff + Math.random() * this.jitterMs);
2200
+ }
2201
+ }
2202
+ };
2203
+
2204
+ // src/replay/player.ts
2205
+ import { EventEmitter as EventEmitter2 } from "events";
2206
+ function parseReplay(content) {
2207
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
2208
+ if (lines.length === 0) throw new Error("empty replay file");
2209
+ const header = JSON.parse(lines[0]);
2210
+ if (header.kind !== "termfc-replay" || typeof header.v !== "number")
2211
+ throw new Error("not a termfc replay file");
2212
+ const seen = /* @__PURE__ */ new Set();
2213
+ const events = [];
2214
+ for (const line of lines.slice(1)) {
2215
+ const e = JSON.parse(line);
2216
+ if (seen.has(e.id)) continue;
2217
+ seen.add(e.id);
2218
+ events.push(e);
2219
+ }
2220
+ events.sort(compareEvents);
2221
+ return { match: header.match, events };
2222
+ }
2223
+ function elapsedSeconds(e) {
2224
+ return (e.minute + (e.injury ?? 0)) * 60 + (e.second ?? 0);
2225
+ }
2226
+ function phaseFor(e, current) {
2227
+ switch (e.period) {
2228
+ case 3:
2229
+ return "FIRST_HALF";
2230
+ case 4:
2231
+ return "HALFTIME";
2232
+ case 5:
2233
+ return "SECOND_HALF";
2234
+ case 7:
2235
+ return "ET_FIRST";
2236
+ case 9:
2237
+ return "ET_SECOND";
2238
+ case 11:
2239
+ return "PENALTIES";
2240
+ case 10:
2241
+ return "FINISHED";
2242
+ }
2243
+ if (e.type === "FULLTIME") return "FINISHED";
2244
+ return current;
2245
+ }
2246
+ var ReplayFeed = class extends EventEmitter2 {
2247
+ constructor(replay, speed = 60) {
2248
+ super();
2249
+ this.replay = replay;
2250
+ this.speed = speed;
2251
+ }
2252
+ replay;
2253
+ speed;
2254
+ stopped = false;
2255
+ start() {
2256
+ void this.run();
2257
+ }
2258
+ stop() {
2259
+ this.stopped = true;
2260
+ }
2261
+ async run() {
2262
+ const { match, events } = this.replay;
2263
+ let score = { home: 0, away: 0 };
2264
+ let phase = "FIRST_HALF";
2265
+ let prevElapsed = 0;
2266
+ for (const e of events) {
2267
+ if (this.stopped) return;
2268
+ const elapsed = elapsedSeconds(e);
2269
+ const gapMs = Math.max(0, elapsed - prevElapsed) * 1e3 / this.speed;
2270
+ const capMs = this.speed <= 1 ? Infinity : 5e3;
2271
+ await sleep(Math.min(gapMs, capMs));
2272
+ if (this.stopped) return;
2273
+ prevElapsed = Math.max(prevElapsed, elapsed);
2274
+ phase = phaseFor(e, phase);
2275
+ if (e.scoreAfter) {
2276
+ score = { ...e.scoreAfter };
2277
+ } else if (CELEBRATION_TYPES.has(e.type) && e.teamSide) {
2278
+ const side = e.type === "OWN_GOAL" ? e.teamSide === "home" ? "away" : "home" : e.teamSide;
2279
+ score = { ...score, [side]: score[side] + 1 };
2280
+ }
2281
+ const cap = regulationCap(phase);
2282
+ const state = {
2283
+ score,
2284
+ phase,
2285
+ minute: cap !== void 0 ? Math.min(e.minute, cap) : e.minute,
2286
+ ...e.injury !== void 0 ? { injury: e.injury } : {},
2287
+ ...e.second !== void 0 ? { second: e.second } : {}
2288
+ };
2289
+ this.emit("state", state);
2290
+ this.emit("events", [e]);
2291
+ if (CELEBRATION_TYPES.has(e.type)) this.emit("goal", e);
2292
+ match.score = score;
2293
+ match.phase = phase;
2294
+ }
2295
+ this.emit("finished");
2296
+ }
2297
+ };
2298
+ function sleep(ms) {
2299
+ if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve();
2300
+ return new Promise((r) => setTimeout(r, ms));
2301
+ }
2302
+
2303
+ // src/replay/recorder.ts
2304
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, statSync } from "fs";
2305
+ import { join as join2 } from "path";
2306
+
2307
+ // node_modules/env-paths/index.js
2308
+ import path from "path";
2309
+ import os from "os";
2310
+ import process2 from "process";
2311
+ var homedir = os.homedir();
2312
+ var tmpdir = os.tmpdir();
2313
+ var { env } = process2;
2314
+ var macos = (name) => {
2315
+ const library = path.join(homedir, "Library");
2316
+ return {
2317
+ data: path.join(library, "Application Support", name),
2318
+ config: path.join(library, "Preferences", name),
2319
+ cache: path.join(library, "Caches", name),
2320
+ log: path.join(library, "Logs", name),
2321
+ temp: path.join(tmpdir, name)
2322
+ };
2323
+ };
2324
+ var windows = (name) => {
2325
+ const appData = env.APPDATA || path.join(homedir, "AppData", "Roaming");
2326
+ const localAppData = env.LOCALAPPDATA || path.join(homedir, "AppData", "Local");
2327
+ return {
2328
+ // Data/config/cache/log are invented by me as Windows isn't opinionated about this
2329
+ data: path.join(localAppData, name, "Data"),
2330
+ config: path.join(appData, name, "Config"),
2331
+ cache: path.join(localAppData, name, "Cache"),
2332
+ log: path.join(localAppData, name, "Log"),
2333
+ temp: path.join(tmpdir, name)
2334
+ };
2335
+ };
2336
+ var linux = (name) => {
2337
+ const username = path.basename(homedir);
2338
+ return {
2339
+ data: path.join(env.XDG_DATA_HOME || path.join(homedir, ".local", "share"), name),
2340
+ config: path.join(env.XDG_CONFIG_HOME || path.join(homedir, ".config"), name),
2341
+ cache: path.join(env.XDG_CACHE_HOME || path.join(homedir, ".cache"), name),
2342
+ // https://wiki.debian.org/XDGBaseDirectorySpecification#state
2343
+ log: path.join(env.XDG_STATE_HOME || path.join(homedir, ".local", "state"), name),
2344
+ temp: path.join(tmpdir, username, name)
2345
+ };
2346
+ };
2347
+ function envPaths(name, { suffix = "nodejs" } = {}) {
2348
+ if (typeof name !== "string") {
2349
+ throw new TypeError(`Expected a string, got ${typeof name}`);
2350
+ }
2351
+ if (suffix) {
2352
+ name += `-${suffix}`;
2353
+ }
2354
+ if (process2.platform === "darwin") {
2355
+ return macos(name);
2356
+ }
2357
+ if (process2.platform === "win32") {
2358
+ return windows(name);
2359
+ }
2360
+ return linux(name);
2361
+ }
2362
+
2363
+ // src/store/store.ts
2364
+ import {
2365
+ existsSync,
2366
+ mkdirSync,
2367
+ readFileSync,
2368
+ renameSync,
2369
+ writeFileSync
2370
+ } from "fs";
2371
+ import { dirname, join } from "path";
2372
+ var paths = envPaths("termfc", { suffix: "" });
2373
+ var CONFIG_FILE = join(paths.config, "config.json");
2374
+ var CACHE_DIR = paths.cache;
2375
+ var REPLAY_DIR = join(paths.cache, "replays");
2376
+ var SCHEDULE_CACHE_FILE = join(CACHE_DIR, "schedule-17-285023.json");
2377
+ var SCHEMA_VERSION = 1;
2378
+ var SCHEDULE_TTL_MS = 30 * 6e4;
2379
+ function atomicWrite(file, data) {
2380
+ mkdirSync(dirname(file), { recursive: true });
2381
+ const tmp = `${file}.tmp-${process.pid}`;
2382
+ writeFileSync(tmp, data);
2383
+ renameSync(tmp, file);
2384
+ }
2385
+ function readConfig() {
2386
+ try {
2387
+ if (!existsSync(CONFIG_FILE)) return {};
2388
+ const cfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
2389
+ return typeof cfg === "object" && cfg !== null ? cfg : {};
2390
+ } catch {
2391
+ return {};
2392
+ }
2393
+ }
2394
+ function writeConfig(cfg) {
2395
+ atomicWrite(CONFIG_FILE, JSON.stringify(cfg, null, 2));
2396
+ }
2397
+ function readScheduleCache(lang) {
2398
+ try {
2399
+ if (!existsSync(SCHEDULE_CACHE_FILE)) return null;
2400
+ const raw = JSON.parse(readFileSync(SCHEDULE_CACHE_FILE, "utf8"));
2401
+ if (raw?.schemaVersion !== SCHEMA_VERSION || raw?.lang !== lang)
2402
+ return null;
2403
+ if (!Array.isArray(raw.matches) || typeof raw.fetchedAt !== "number")
2404
+ return null;
2405
+ return { fetchedAt: raw.fetchedAt, lang: raw.lang, matches: raw.matches };
2406
+ } catch {
2407
+ return null;
2408
+ }
2409
+ }
2410
+ function writeScheduleCache(matches, lang) {
2411
+ atomicWrite(
2412
+ SCHEDULE_CACHE_FILE,
2413
+ JSON.stringify({
2414
+ schemaVersion: SCHEMA_VERSION,
2415
+ fetchedAt: Date.now(),
2416
+ lang,
2417
+ matches
2418
+ })
2419
+ );
2420
+ }
2421
+ function isScheduleFresh(cache) {
2422
+ return Date.now() - cache.fetchedAt < SCHEDULE_TTL_MS;
2423
+ }
2424
+
2425
+ // src/replay/recorder.ts
2426
+ var REPLAY_FORMAT_VERSION = 1;
2427
+ var ReplayRecorder = class {
2428
+ constructor(match, dir = REPLAY_DIR) {
2429
+ this.match = match;
2430
+ mkdirSync2(dir, { recursive: true });
2431
+ const slug = `${match.id}-${match.home.code}-${match.away.code}`.replace(
2432
+ /[^\w.-]/g,
2433
+ "_"
2434
+ );
2435
+ this.file = join2(dir, `${slug}.jsonl`);
2436
+ const empty = !existsSync2(this.file) || statSync(this.file).size === 0;
2437
+ if (empty) {
2438
+ const header = {
2439
+ v: REPLAY_FORMAT_VERSION,
2440
+ kind: "termfc-replay",
2441
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
2442
+ match
2443
+ };
2444
+ appendFileSync(this.file, `${JSON.stringify(header)}
2445
+ `);
2446
+ }
2447
+ }
2448
+ match;
2449
+ file;
2450
+ append(events) {
2451
+ if (events.length === 0) return;
2452
+ const lines = events.map((e) => JSON.stringify(e)).join("\n");
2453
+ appendFileSync(this.file, `${lines}
2454
+ `);
2455
+ }
2456
+ };
2457
+
2458
+ // node_modules/ansi-regex/index.js
2459
+ function ansiRegex({ onlyFirst = false } = {}) {
2460
+ const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
2461
+ const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`;
2462
+ const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
2463
+ const pattern = `${osc}|${csi}`;
2464
+ return new RegExp(pattern, onlyFirst ? void 0 : "g");
2465
+ }
2466
+
2467
+ // node_modules/strip-ansi/index.js
2468
+ var regex = ansiRegex();
2469
+ function stripAnsi(string) {
2470
+ if (typeof string !== "string") {
2471
+ throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
2472
+ }
2473
+ if (!string.includes("\x1B") && !string.includes("\x9B")) {
2474
+ return string;
2475
+ }
2476
+ return string.replace(regex, "");
2477
+ }
2478
+
2479
+ // node_modules/get-east-asian-width/lookup-data.js
2480
+ var ambiguousMinimalCodePoint = 161;
2481
+ var ambiguousMaximumCodePoint = 1114109;
2482
+ var ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
2483
+ var fullwidthMinimalCodePoint = 12288;
2484
+ var fullwidthMaximumCodePoint = 65510;
2485
+ var fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
2486
+ var wideMinimalCodePoint = 4352;
2487
+ var wideMaximumCodePoint = 262141;
2488
+ var wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101589, 101631, 101662, 101760, 101874, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110882, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110951, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128728, 128732, 128735, 128747, 128748, 128756, 128764, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129674, 129678, 129734, 129736, 129736, 129741, 129756, 129759, 129770, 129775, 129784, 131072, 196605, 196608, 262141];
2489
+
2490
+ // node_modules/get-east-asian-width/utilities.js
2491
+ var isInRange = (ranges, codePoint) => {
2492
+ let low = 0;
2493
+ let high = Math.floor(ranges.length / 2) - 1;
2494
+ while (low <= high) {
2495
+ const mid = Math.floor((low + high) / 2);
2496
+ const i = mid * 2;
2497
+ if (codePoint < ranges[i]) {
2498
+ high = mid - 1;
2499
+ } else if (codePoint > ranges[i + 1]) {
2500
+ low = mid + 1;
2501
+ } else {
2502
+ return true;
2503
+ }
2504
+ }
2505
+ return false;
2506
+ };
2507
+
2508
+ // node_modules/get-east-asian-width/lookup.js
2509
+ var commonCjkCodePoint = 19968;
2510
+ var [wideFastPathStart, wideFastPathEnd] = /* @__PURE__ */ findWideFastPathRange(wideRanges);
2511
+ function findWideFastPathRange(ranges) {
2512
+ let fastPathStart = ranges[0];
2513
+ let fastPathEnd = ranges[1];
2514
+ for (let index = 0; index < ranges.length; index += 2) {
2515
+ const start = ranges[index];
2516
+ const end = ranges[index + 1];
2517
+ if (commonCjkCodePoint >= start && commonCjkCodePoint <= end) {
2518
+ return [start, end];
2519
+ }
2520
+ if (end - start > fastPathEnd - fastPathStart) {
2521
+ fastPathStart = start;
2522
+ fastPathEnd = end;
2523
+ }
2524
+ }
2525
+ return [fastPathStart, fastPathEnd];
2526
+ }
2527
+ var isAmbiguous = (codePoint) => {
2528
+ if (codePoint < ambiguousMinimalCodePoint || codePoint > ambiguousMaximumCodePoint) {
2529
+ return false;
2530
+ }
2531
+ return isInRange(ambiguousRanges, codePoint);
2532
+ };
2533
+ var isFullWidth = (codePoint) => {
2534
+ if (codePoint < fullwidthMinimalCodePoint || codePoint > fullwidthMaximumCodePoint) {
2535
+ return false;
2536
+ }
2537
+ return isInRange(fullwidthRanges, codePoint);
2538
+ };
2539
+ var isWide = (codePoint) => {
2540
+ if (codePoint >= wideFastPathStart && codePoint <= wideFastPathEnd) {
2541
+ return true;
2542
+ }
2543
+ if (codePoint < wideMinimalCodePoint || codePoint > wideMaximumCodePoint) {
2544
+ return false;
2545
+ }
2546
+ return isInRange(wideRanges, codePoint);
2547
+ };
2548
+
2549
+ // node_modules/get-east-asian-width/index.js
2550
+ function validate(codePoint) {
2551
+ if (!Number.isSafeInteger(codePoint)) {
2552
+ throw new TypeError(`Expected a code point, got \`${typeof codePoint}\`.`);
2553
+ }
2554
+ }
2555
+ function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
2556
+ validate(codePoint);
2557
+ if (isFullWidth(codePoint) || isWide(codePoint) || ambiguousAsWide && isAmbiguous(codePoint)) {
2558
+ return 2;
2559
+ }
2560
+ return 1;
2561
+ }
2562
+
2563
+ // node_modules/emoji-regex/index.mjs
2564
+ var emoji_regex_default = () => {
2565
+ return /[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E-\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])))?))?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3C-\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC2\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF]|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g;
2566
+ };
2567
+
2568
+ // node_modules/string-width/index.js
2569
+ var segmenter = new Intl.Segmenter();
2570
+ var defaultIgnorableCodePointRegex = new RegExp("^\\p{Default_Ignorable_Code_Point}$", "u");
2571
+ function stringWidth(string, options = {}) {
2572
+ if (typeof string !== "string" || string.length === 0) {
2573
+ return 0;
2574
+ }
2575
+ const {
2576
+ ambiguousIsNarrow = true,
2577
+ countAnsiEscapeCodes = false
2578
+ } = options;
2579
+ if (!countAnsiEscapeCodes) {
2580
+ string = stripAnsi(string);
2581
+ }
2582
+ if (string.length === 0) {
2583
+ return 0;
2584
+ }
2585
+ let width2 = 0;
2586
+ const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
2587
+ for (const { segment: character } of segmenter.segment(string)) {
2588
+ const codePoint = character.codePointAt(0);
2589
+ if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) {
2590
+ continue;
2591
+ }
2592
+ if (codePoint >= 8203 && codePoint <= 8207 || codePoint === 65279) {
2593
+ continue;
2594
+ }
2595
+ if (codePoint >= 768 && codePoint <= 879 || codePoint >= 6832 && codePoint <= 6911 || codePoint >= 7616 && codePoint <= 7679 || codePoint >= 8400 && codePoint <= 8447 || codePoint >= 65056 && codePoint <= 65071) {
2596
+ continue;
2597
+ }
2598
+ if (codePoint >= 55296 && codePoint <= 57343) {
2599
+ continue;
2600
+ }
2601
+ if (codePoint >= 65024 && codePoint <= 65039) {
2602
+ continue;
2603
+ }
2604
+ if (defaultIgnorableCodePointRegex.test(character)) {
2605
+ continue;
2606
+ }
2607
+ if (emoji_regex_default().test(character)) {
2608
+ width2 += 2;
2609
+ continue;
2610
+ }
2611
+ width2 += eastAsianWidth(codePoint, eastAsianWidthOptions);
2612
+ }
2613
+ return width2;
2614
+ }
2615
+
2616
+ // src/ui/ansi.ts
2617
+ var enabled = process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb" && (process.stdout.isTTY ?? false);
2618
+ function sgr(open, close) {
2619
+ return (s) => enabled ? `\x1B[${open}m${s}\x1B[${close}m` : s;
2620
+ }
2621
+ var bold = sgr(1, 22);
2622
+ var dim = sgr(2, 22);
2623
+ var inverse = sgr(7, 27);
2624
+ var red = sgr(31, 39);
2625
+ var green = sgr(32, 39);
2626
+ var yellow = sgr(33, 39);
2627
+ var blue = sgr(34, 39);
2628
+ var magenta = sgr(35, 39);
2629
+ var cyan = sgr(36, 39);
2630
+ var gray = sgr(90, 39);
2631
+ var bgYellow = sgr("43;30", "49;39");
2632
+ var bgGreen = sgr("42;30", "49;39");
2633
+ var bgRed = sgr("41;97", "49;39");
2634
+ function width(s) {
2635
+ return stringWidth(s.replace(/\x1b\[[0-9;]*m/g, ""));
2636
+ }
2637
+ function truncate(s, max) {
2638
+ if (width(s) <= max) return s;
2639
+ let out = "";
2640
+ let w = 0;
2641
+ const parts = s.split(/(\x1b\[[0-9;]*m)/);
2642
+ for (const part of parts) {
2643
+ if (part.startsWith("\x1B[")) {
2644
+ out += part;
2645
+ continue;
2646
+ }
2647
+ for (const ch of part) {
2648
+ const cw = stringWidth(ch);
2649
+ if (w + cw > max - 1) return `${out}\u2026${enabled ? "\x1B[0m" : ""}`;
2650
+ out += ch;
2651
+ w += cw;
2652
+ }
2653
+ }
2654
+ return out;
2655
+ }
2656
+ function center(s, cols) {
2657
+ const w = width(s);
2658
+ if (w >= cols) return s;
2659
+ return " ".repeat(Math.floor((cols - w) / 2)) + s;
2660
+ }
2661
+ function term(out = process.stdout) {
2662
+ return {
2663
+ out,
2664
+ cols: () => out.columns ?? 80,
2665
+ rows: () => out.rows ?? 24
2666
+ };
2667
+ }
2668
+ function enterAltScreen(out) {
2669
+ if (out.isTTY) out.write("\x1B[?1049h\x1B[?25l");
2670
+ }
2671
+ function leaveAltScreen(out) {
2672
+ if (out.isTTY) out.write("\x1B[?25h\x1B[?1049l");
2673
+ }
2674
+ function clearScreen(out) {
2675
+ out.write(out.isTTY ? "\x1B[2J\x1B[H" : "\n");
2676
+ }
2677
+
2678
+ // src/ui/banner.ts
2679
+ var ART = [
2680
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
2681
+ "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
2682
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 ",
2683
+ " \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 ",
2684
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
2685
+ " \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D"
2686
+ ];
2687
+ function renderBanner(version) {
2688
+ const colored = ART.map(
2689
+ (line, i) => i < 3 ? green(line) : cyan(line)
2690
+ ).join("\n");
2691
+ const info = [
2692
+ "",
2693
+ ` \u26BD ${bold("termfc")} v${version} \u2014 FIFA World Cup 2026\u2122 live text commentary`,
2694
+ dim(" data: fifa.com (primary) \xB7 espn.com (fallback) \xB7 unofficial public endpoints"),
2695
+ ""
2696
+ ].join("\n");
2697
+ return `
2698
+ ${colored}${info}`;
2699
+ }
2700
+
2701
+ // src/ui/listScreen.ts
2702
+ import * as readline from "readline/promises";
2703
+ function kickoffLabel(iso, lang) {
2704
+ const d = new Date(iso);
2705
+ return new Intl.DateTimeFormat(lang === "ko" ? "ko-KR" : "en-US", {
2706
+ month: "short",
2707
+ day: "numeric",
2708
+ weekday: "short",
2709
+ hour: "2-digit",
2710
+ minute: "2-digit"
2711
+ }).format(d);
2712
+ }
2713
+ function matchLine(m, lang) {
2714
+ const l = labels(lang);
2715
+ const vs = `${m.home.flag} ${m.home.name}`;
2716
+ const vs2 = `${m.away.name} ${m.away.flag}`;
2717
+ if (isLivePhase(m.phase)) {
2718
+ const t = m.matchTime ? ` ${m.matchTime}` : "";
2719
+ return `${red("\u25CF")}${bold(t.padEnd(8))} ${vs} ${bold(`${m.score.home} : ${m.score.away}`)} ${vs2} ${dim(phaseLabel(m.phase, l))}`;
2720
+ }
2721
+ if (m.phase === "FINISHED") {
2722
+ const pens = m.score.penHome !== void 0 ? ` (PSO ${m.score.penHome}-${m.score.penAway})` : "";
2723
+ return gray(
2724
+ `FT ${m.home.name} ${m.score.home} : ${m.score.away}${pens} ${m.away.name}`
2725
+ );
2726
+ }
2727
+ return `${dim(kickoffLabel(m.kickoff, lang).padEnd(20))} ${vs} ${dim("vs")} ${vs2}`;
2728
+ }
2729
+ function fmtCountdown(ms) {
2730
+ const s = Math.max(0, Math.floor(ms / 1e3));
2731
+ const h = Math.floor(s / 3600);
2732
+ const m = Math.floor(s % 3600 / 60);
2733
+ const sec = s % 60;
2734
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
2735
+ }
2736
+ function sections(matches, lang) {
2737
+ const l = labels(lang);
2738
+ const live = matches.filter((m) => isLivePhase(m.phase));
2739
+ const upcoming = matches.filter((m) => m.phase === "SCHEDULED");
2740
+ const finished = matches.filter((m) => m.phase === "FINISHED" || m.phase === "ABANDONED").slice(-6);
2741
+ return [
2742
+ { title: l.liveNow, matches: live },
2743
+ { title: l.upcoming, matches: upcoming },
2744
+ { title: l.recentResults, matches: finished }
2745
+ ];
2746
+ }
2747
+ function renderList(matches, opts) {
2748
+ const l = labels(opts.lang);
2749
+ const out = process.stdout;
2750
+ const pickable = [];
2751
+ if (opts.staleAt) {
2752
+ const at = new Date(opts.staleAt).toTimeString().slice(0, 5);
2753
+ out.write(yellow(` \u26A0 ${l.offline} \xB7 ${at} ${l.staleData}
2754
+
2755
+ `));
2756
+ }
2757
+ for (const sec of sections(matches, opts.lang)) {
2758
+ if (sec.matches.length === 0) continue;
2759
+ out.write(` ${bold(cyan(sec.title))}
2760
+ `);
2761
+ for (const m of sec.matches) {
2762
+ pickable.push(m);
2763
+ const no = String(pickable.length).padStart(2);
2764
+ out.write(` ${dim(no + ".")} ${matchLine(m, opts.lang)}
2765
+ `);
2766
+ }
2767
+ out.write("\n");
2768
+ }
2769
+ const anyLive = matches.some((m) => isLivePhase(m.phase));
2770
+ if (!anyLive) {
2771
+ const next = matches.filter((m) => m.phase === "SCHEDULED").sort((a, b) => a.kickoff.localeCompare(b.kickoff))[0];
2772
+ if (next) {
2773
+ const ms = new Date(next.kickoff).getTime() - Date.now();
2774
+ out.write(
2775
+ ` ${green("\u23F3")} ${l.noLiveMatches} \u2014 ${l.nextMatchIn} ${bold(fmtCountdown(ms))} (${next.home.name} vs ${next.away.name})
2776
+
2777
+ `
2778
+ );
2779
+ } else {
2780
+ out.write(` ${dim(l.noLiveMatches)}
2781
+
2782
+ `);
2783
+ }
2784
+ }
2785
+ return pickable;
2786
+ }
2787
+ async function pickMatch(pickable, lang) {
2788
+ if (pickable.length === 0) return null;
2789
+ const l = labels(lang);
2790
+ const rl = readline.createInterface({
2791
+ input: process.stdin,
2792
+ output: process.stdout
2793
+ });
2794
+ try {
2795
+ while (true) {
2796
+ const answer = (await rl.question(` ${l.pickPrompt}`)).trim();
2797
+ if (answer === "q" || answer === "Q" || answer === "") return null;
2798
+ const n = Number(answer);
2799
+ if (Number.isInteger(n) && n >= 1 && n <= pickable.length) {
2800
+ return pickable[n - 1] ?? null;
2801
+ }
2802
+ }
2803
+ } finally {
2804
+ rl.close();
2805
+ }
2806
+ }
2807
+
2808
+ // src/ui/matchScreen.ts
2809
+ import * as readline2 from "readline";
2810
+
2811
+ // src/core/clock.ts
2812
+ var ClockInterpolator = class {
2813
+ /** `rate` > 1 makes the clock run faster than wall time (replay mode). */
2814
+ constructor(nowMs = () => performance.now(), rate = 1) {
2815
+ this.nowMs = nowMs;
2816
+ this.rate = rate;
2817
+ this.anchor = this.nowMs();
2818
+ }
2819
+ nowMs;
2820
+ rate;
2821
+ baseSeconds = 0;
2822
+ anchor = 0;
2823
+ phase = "SCHEDULED";
2824
+ sourceMinute = 0;
2825
+ sourceInjury;
2826
+ get currentPhase() {
2827
+ return this.phase;
2828
+ }
2829
+ update(opts) {
2830
+ const now = this.nowMs();
2831
+ const running = isClockRunning(opts.phase);
2832
+ const cap = regulationCap(opts.phase);
2833
+ const effectiveMinute = opts.injury !== void 0 && cap !== void 0 ? cap + opts.injury : opts.minute;
2834
+ let sourceSeconds = opts.second !== void 0 ? opts.minute * 60 + opts.second : effectiveMinute * 60;
2835
+ const current = this.peekSeconds(now);
2836
+ const phaseChanged = opts.phase !== this.phase;
2837
+ if (phaseChanged) {
2838
+ if (opts.phase === "FINISHED" || opts.phase === "ABANDONED") {
2839
+ this.baseSeconds = Math.max(current, sourceSeconds);
2840
+ } else {
2841
+ this.baseSeconds = Math.max(sourceSeconds, phaseStartSeconds(opts.phase));
2842
+ }
2843
+ this.anchor = now;
2844
+ } else if (sourceSeconds > current) {
2845
+ this.baseSeconds = sourceSeconds;
2846
+ this.anchor = now;
2847
+ } else if (!running) {
2848
+ this.baseSeconds = this.peekSeconds(now);
2849
+ this.anchor = now;
2850
+ }
2851
+ this.phase = opts.phase;
2852
+ this.sourceMinute = opts.minute;
2853
+ this.sourceInjury = opts.injury;
2854
+ }
2855
+ display() {
2856
+ const now = this.nowMs();
2857
+ const total = Math.floor(this.peekSeconds(now));
2858
+ const running = isClockRunning(this.phase);
2859
+ const cap = regulationCap(this.phase);
2860
+ if (cap !== void 0 && total >= cap * 60 && (this.sourceInjury !== void 0 || total > cap * 60)) {
2861
+ const over = total - cap * 60;
2862
+ return {
2863
+ totalSeconds: total,
2864
+ minute: cap,
2865
+ second: 0,
2866
+ injuryMinute: Math.floor(over / 60),
2867
+ injurySecond: over % 60,
2868
+ running
2869
+ };
2870
+ }
2871
+ return {
2872
+ totalSeconds: total,
2873
+ minute: Math.floor(total / 60),
2874
+ second: total % 60,
2875
+ running
2876
+ };
2877
+ }
2878
+ peekSeconds(now) {
2879
+ if (!isClockRunning(this.phase)) return this.baseSeconds;
2880
+ return this.baseSeconds + (now - this.anchor) / 1e3 * this.rate;
2881
+ }
2882
+ };
2883
+ function phaseStartSeconds(phase) {
2884
+ switch (phase) {
2885
+ case "HALFTIME":
2886
+ case "SECOND_HALF":
2887
+ return 45 * 60;
2888
+ case "ET_BREAK":
2889
+ case "ET_FIRST":
2890
+ return 90 * 60;
2891
+ case "ET_SECOND":
2892
+ return 105 * 60;
2893
+ case "PENALTIES":
2894
+ return 120 * 60;
2895
+ default:
2896
+ return 0;
2897
+ }
2898
+ }
2899
+ function formatClock(d) {
2900
+ const mm = String(d.minute).padStart(2, "0");
2901
+ const ss = String(d.second).padStart(2, "0");
2902
+ if (d.injuryMinute !== void 0) {
2903
+ const im = d.injuryMinute;
2904
+ const is = String(d.injurySecond ?? 0).padStart(2, "0");
2905
+ return `${mm}:${ss} +${im}:${is}`;
2906
+ }
2907
+ return `${mm}:${ss}`;
2908
+ }
2909
+
2910
+ // src/ui/animations.ts
2911
+ function sleep2(ms) {
2912
+ return new Promise((r) => setTimeout(r, ms));
2913
+ }
2914
+ var AnimationController = class {
2915
+ constructor(drawFrame) {
2916
+ this.drawFrame = drawFrame;
2917
+ }
2918
+ drawFrame;
2919
+ mode = "LIVE";
2920
+ skipFlag = false;
2921
+ chain = Promise.resolve();
2922
+ queued = 0;
2923
+ maxQueued = 3;
2924
+ skip() {
2925
+ this.skipFlag = true;
2926
+ }
2927
+ /** Queue an animation; resolves when this animation finishes. */
2928
+ enqueue(frames, fps = 8) {
2929
+ if (this.queued >= this.maxQueued) return this.chain;
2930
+ this.queued++;
2931
+ this.chain = this.chain.then(async () => {
2932
+ this.mode = "ANIMATION";
2933
+ this.skipFlag = false;
2934
+ const delay = 1e3 / fps;
2935
+ for (const f of frames) {
2936
+ if (this.skipFlag) break;
2937
+ this.drawFrame(f);
2938
+ await sleep2(delay);
2939
+ }
2940
+ this.queued--;
2941
+ if (this.queued === 0) this.mode = "LIVE";
2942
+ });
2943
+ return this.chain;
2944
+ }
2945
+ /** Wait until everything queued has finished. */
2946
+ idle() {
2947
+ return this.chain;
2948
+ }
2949
+ };
2950
+ var GOAL_ART = [
2951
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557",
2952
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551",
2953
+ "\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
2954
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D",
2955
+ "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557",
2956
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D"
2957
+ ];
2958
+ var TROPHY = [
2959
+ " ___________ ",
2960
+ " '._==_==_=_.' ",
2961
+ " .-\\: /-. ",
2962
+ " | (|:. |) | ",
2963
+ " '-|:. |-' ",
2964
+ " \\::. / ",
2965
+ " '::. .' ",
2966
+ " ) ( ",
2967
+ " _.' '._ ",
2968
+ ' `"""""""` '
2969
+ ];
2970
+ function goalFrames(opts) {
2971
+ const { cols } = opts;
2972
+ const frames = [];
2973
+ const track = Math.max(10, Math.min(cols - 10, 40));
2974
+ for (let i = 0; i <= track; i += Math.ceil(track / 5)) {
2975
+ frames.push([
2976
+ "",
2977
+ "",
2978
+ center(`${" ".repeat(i)}\u26BD`, cols),
2979
+ center(dim("\u2500".repeat(track)), cols),
2980
+ ""
2981
+ ]);
2982
+ }
2983
+ const paints = [yellow, green, magenta, cyan, yellow, green];
2984
+ const who = [opts.flag, opts.player, opts.teamName && `(${opts.teamName})`].filter(Boolean).join(" ");
2985
+ for (let i = 0; i < paints.length; i++) {
2986
+ const paint = paints[i];
2987
+ frames.push([
2988
+ "",
2989
+ ...GOAL_ART.map((l) => center(paint(bold(l)), cols)),
2990
+ "",
2991
+ center(bold(who || "GOAL!"), cols),
2992
+ ""
2993
+ ]);
2994
+ }
2995
+ return frames;
2996
+ }
2997
+ function entranceFrames(match, cols) {
2998
+ const title = "FIFA WORLD CUP 2026\u2122";
2999
+ const tie = `${match.home.flag} ${match.home.name} VS ${match.away.name} ${match.away.flag}`;
3000
+ const stage = match.stage;
3001
+ const full = [
3002
+ "",
3003
+ center(bold(yellow(title)), cols),
3004
+ "",
3005
+ ...TROPHY.map((l) => center(yellow(l), cols)),
3006
+ "",
3007
+ center(bold(tie), cols),
3008
+ stage ? center(dim(stage), cols) : "",
3009
+ ""
3010
+ ];
3011
+ const frames = [];
3012
+ const steps = 6;
3013
+ for (let s = 1; s <= steps; s++) {
3014
+ const upto = Math.ceil(full.length * s / steps);
3015
+ frames.push(full.slice(0, upto));
3016
+ }
3017
+ const shineNames = [
3018
+ center(bold(green(tie)), cols),
3019
+ center(bold(cyan(tie)), cols),
3020
+ center(bold(tie), cols)
3021
+ ];
3022
+ for (const n of shineNames) {
3023
+ const f = [...full];
3024
+ f[full.length - 3] = n;
3025
+ frames.push(f);
3026
+ }
3027
+ return frames;
3028
+ }
3029
+
3030
+ // src/ui/matchScreen.ts
3031
+ var MatchScreen = class {
3032
+ constructor(match, feed, opts) {
3033
+ this.match = match;
3034
+ this.feed = feed;
3035
+ this.opts = opts;
3036
+ this.l = labels(opts.lang);
3037
+ this.clock = new ClockInterpolator(void 0, opts.clockRate ?? 1);
3038
+ this.controller = new AnimationController((frame) => {
3039
+ clearScreen(this.t.out);
3040
+ const pad = Math.max(0, Math.floor((this.t.rows() - frame.length) / 2));
3041
+ this.t.out.write("\n".repeat(pad) + frame.join("\n") + "\n");
3042
+ });
3043
+ if (match.phase !== "SCHEDULED") {
3044
+ this.clock.update({ minute: 0, phase: match.phase });
3045
+ }
3046
+ }
3047
+ match;
3048
+ feed;
3049
+ opts;
3050
+ t = term();
3051
+ l;
3052
+ clock;
3053
+ controller;
3054
+ lines = [];
3055
+ pending = [];
3056
+ state = null;
3057
+ net = null;
3058
+ lastUpdateAt = null;
3059
+ flashUntil = 0;
3060
+ flashText = "";
3061
+ finished = false;
3062
+ sourceSwitchedLabel = false;
3063
+ async run() {
3064
+ const { out } = this.t;
3065
+ enterAltScreen(out);
3066
+ const input = process.stdin;
3067
+ readline2.emitKeypressEvents(input);
3068
+ if (input.isTTY) input.setRawMode(true);
3069
+ input.resume();
3070
+ let resolveDone;
3071
+ const done = new Promise((r) => resolveDone = r);
3072
+ const onKey = (_str, key) => {
3073
+ const name = key?.name ?? _str;
3074
+ if (name === "q" || name === "escape" || key?.ctrl && name === "c") {
3075
+ resolveDone();
3076
+ } else if (name === "s") {
3077
+ this.controller.skip();
3078
+ } else if (this.finished) {
3079
+ resolveDone();
3080
+ }
3081
+ };
3082
+ input.on("keypress", onKey);
3083
+ if (this.opts.animations) {
3084
+ await this.controller.enqueue(entranceFrames(this.match, this.t.cols()), 6);
3085
+ }
3086
+ this.subscribe();
3087
+ this.feed.start();
3088
+ const tick = setInterval(() => {
3089
+ if (this.controller.mode === "LIVE") this.render();
3090
+ }, 1e3);
3091
+ const onResize = () => this.render();
3092
+ out.on("resize", onResize);
3093
+ this.render();
3094
+ await done;
3095
+ clearInterval(tick);
3096
+ out.off("resize", onResize);
3097
+ input.off("keypress", onKey);
3098
+ if (input.isTTY) input.setRawMode(false);
3099
+ input.pause();
3100
+ this.feed.stop();
3101
+ leaveAltScreen(out);
3102
+ }
3103
+ subscribe() {
3104
+ this.feed.on("state", (s) => {
3105
+ this.state = s;
3106
+ this.lastUpdateAt = Date.now();
3107
+ this.clock.update({
3108
+ minute: s.minute,
3109
+ second: s.second,
3110
+ injury: s.injury,
3111
+ phase: s.phase
3112
+ });
3113
+ });
3114
+ this.feed.on("events", (events) => {
3115
+ this.opts.onEvents?.(events);
3116
+ const formatted = events.filter((e) => e.type !== "ASSIST" || e.text).map((e) => this.formatEvent(e));
3117
+ if (this.controller.mode === "ANIMATION") this.pending.push(...formatted);
3118
+ else {
3119
+ this.lines.push(...formatted);
3120
+ this.render();
3121
+ }
3122
+ });
3123
+ this.feed.on("goal", (e) => {
3124
+ const side = e.teamSide;
3125
+ const teamName = side ? this.match[side].name : void 0;
3126
+ const flag = side ? this.match[side].flag : "\u26BD";
3127
+ this.flashUntil = Date.now() + 6e3;
3128
+ this.flashText = e.player ? `GOAL! ${e.player}` : "GOAL!";
3129
+ if (this.opts.animations) {
3130
+ void this.controller.enqueue(
3131
+ goalFrames({
3132
+ player: e.player,
3133
+ teamName,
3134
+ flag,
3135
+ cols: this.t.cols()
3136
+ })
3137
+ ).then(() => {
3138
+ this.flushPending();
3139
+ this.render();
3140
+ });
3141
+ }
3142
+ });
3143
+ this.feed.on("cancelled", (e) => {
3144
+ const line = `${dim(formatEventClock(e).padStart(7))} ${red(`\u{1F4FA} ${this.l.goalCancelled} (${this.l.corrected})`)} ${dim(e.text ?? "")}`;
3145
+ this.pushLine(line);
3146
+ });
3147
+ this.feed.on("sourceSwitched", () => {
3148
+ this.sourceSwitchedLabel = true;
3149
+ this.pushLine(yellow(`\u2500\u2500 ${this.l.sourceSwitched} \u2500\u2500`));
3150
+ });
3151
+ this.feed.on("net", (info) => {
3152
+ this.net = info;
3153
+ this.render();
3154
+ });
3155
+ this.feed.on("finished", () => {
3156
+ this.finished = true;
3157
+ this.render();
3158
+ });
3159
+ }
3160
+ pushLine(line) {
3161
+ if (this.controller.mode === "ANIMATION") this.pending.push(line);
3162
+ else {
3163
+ this.lines.push(line);
3164
+ this.render();
3165
+ }
3166
+ }
3167
+ flushPending() {
3168
+ if (this.pending.length > 0) {
3169
+ this.lines.push(...this.pending);
3170
+ this.pending = [];
3171
+ }
3172
+ }
3173
+ formatEvent(e) {
3174
+ const clock = dim(formatEventClock(e).padStart(7));
3175
+ const icon = EVENT_ICON[e.type] ?? "\xB7";
3176
+ let text = e.text ?? "";
3177
+ if (e.type === "GOAL" || e.type === "PENALTY_GOAL" || e.type === "OWN_GOAL")
3178
+ text = bold(yellow(text));
3179
+ else if (e.type === "RED") text = bold(red(text));
3180
+ else if (e.type === "YELLOW") text = yellow(text);
3181
+ else if (e.type === "PERIOD_START" || e.type === "PERIOD_END" || e.type === "FULLTIME")
3182
+ text = cyan(text);
3183
+ else if (e.type === "UNKNOWN") text = dim(text);
3184
+ return `${clock} ${icon} ${text}`;
3185
+ }
3186
+ winprob() {
3187
+ if (this.opts.lambdas === null) return null;
3188
+ const phase = this.state?.phase ?? this.match.phase;
3189
+ if (phase === "SCHEDULED" || phase === "ET_FIRST" || phase === "ET_SECOND" || phase === "ET_BREAK" || phase === "PENALTIES" || phase === "ABANDONED")
3190
+ return null;
3191
+ const d = this.clock.display();
3192
+ const score = this.state?.score ?? this.match.score;
3193
+ return liveWinProb({
3194
+ scoreHome: score.home,
3195
+ scoreAway: score.away,
3196
+ elapsedFraction: Math.min(1, d.totalSeconds / (90 * 60)),
3197
+ lambdas: this.opts.lambdas ?? void 0
3198
+ });
3199
+ }
3200
+ render() {
3201
+ if (this.controller.mode === "ANIMATION") return;
3202
+ const { out } = this.t;
3203
+ const cols = this.t.cols();
3204
+ const rows = this.t.rows();
3205
+ const score = this.state?.score ?? this.match.score;
3206
+ const phase = this.state?.phase ?? this.match.phase;
3207
+ const pens = score.penHome !== void 0 ? dim(` (PSO ${score.penHome}-${score.penAway})`) : "";
3208
+ let scoreStr = `${bold(String(score.home))} : ${bold(String(score.away))}`;
3209
+ if (Date.now() < this.flashUntil) {
3210
+ scoreStr = bgYellow(` ${score.home} : ${score.away} `) + ` ${bold(yellow(this.flashText))}`;
3211
+ }
3212
+ const header1 = center(
3213
+ `${this.match.home.flag} ${bold(this.match.home.name)} ${scoreStr} ${bold(this.match.away.name)} ${this.match.away.flag}${pens}`,
3214
+ cols
3215
+ );
3216
+ const clockStr = isClockRunning(phase) || phase === "HALFTIME" || phase === "FINISHED" ? formatClock(this.clock.display()) : "--:--";
3217
+ let line2 = `\u23F1 ${bold(green(clockStr))} ${phaseLabel(phase, this.l)}`;
3218
+ const wp = this.winprob();
3219
+ if (wp) {
3220
+ const pct = (x) => `${Math.round(x * 100)}%`;
3221
+ line2 += dim(
3222
+ ` ${this.l.winprob} ${this.match.home.code} ${pct(wp.home)} \xB7 ${this.l.draw} ${pct(wp.draw)} \xB7 ${this.match.away.code} ${pct(wp.away)}`
3223
+ );
3224
+ }
3225
+ const header2 = center(line2, cols);
3226
+ const sep = dim("\u2500".repeat(Math.max(4, cols - 2)));
3227
+ const bodyRows = Math.max(3, rows - 6);
3228
+ const body = this.lines.slice(-bodyRows).map((l) => truncate(l, cols - 1));
3229
+ const footer = this.footerLine();
3230
+ const frame = [
3231
+ truncate(header1, cols),
3232
+ truncate(header2, cols),
3233
+ sep,
3234
+ ...body,
3235
+ ...Array(Math.max(0, bodyRows - body.length)).fill(""),
3236
+ sep,
3237
+ truncate(footer, cols)
3238
+ ].join("\n");
3239
+ clearScreen(out);
3240
+ out.write(frame);
3241
+ }
3242
+ footerLine() {
3243
+ const l = this.l;
3244
+ const time = (ts) => new Date(ts).toTimeString().slice(0, 8);
3245
+ const src = this.opts.sourceLabel?.() ?? "";
3246
+ const badge = this.opts.mode === "replay" ? cyan(` ${l.replaying}${this.opts.speedLabel ? ` \xD7${this.opts.speedLabel}` : ""} `) : this.finished ? gray(` ${l.finished} `) : green(bold(" LIVE "));
3247
+ if (this.net?.down) {
3248
+ const retry = this.net.retryInMs ? ` \xB7 ${l.retryIn} ${Math.round(this.net.retryInMs / 1e3)}s` : "";
3249
+ const last = this.net.lastOkAt ? ` \xB7 ${l.lastUpdate} ${time(this.net.lastOkAt)}` : "";
3250
+ return `${badge} ${yellow(`\u26A0 ${l.reconnecting}${retry}${last}`)} ${dim(l.quitHint)}`;
3251
+ }
3252
+ const upd = this.lastUpdateAt ? `${l.lastUpdate} ${time(this.lastUpdateAt)}` : l.loading;
3253
+ const switched = this.sourceSwitchedLabel ? yellow(" \u21AF") : "";
3254
+ return `${badge} ${dim(`${src}${switched} \xB7 ${upd}`)} ${dim(l.quitHint)}`;
3255
+ }
3256
+ };
3257
+
3258
+ // package.json
3259
+ var package_default = {
3260
+ name: "termfc",
3261
+ version: "0.2.0",
3262
+ description: "\u26BD Live football text commentary in your terminal \u2014 FIFA World Cup 2026",
3263
+ type: "module",
3264
+ bin: {
3265
+ termfc: "dist/cli.js"
3266
+ },
3267
+ files: [
3268
+ "dist",
3269
+ "README.md",
3270
+ "LICENSE"
3271
+ ],
3272
+ engines: {
3273
+ node: ">=20"
3274
+ },
3275
+ scripts: {
3276
+ build: "tsup",
3277
+ dev: "tsup --watch",
3278
+ test: "vitest run",
3279
+ typecheck: "tsc --noEmit",
3280
+ prepublishOnly: "npm run build && npm test"
3281
+ },
3282
+ repository: {
3283
+ type: "git",
3284
+ url: "git+https://github.com/juunghyun/termfc.git"
3285
+ },
3286
+ keywords: [
3287
+ "football",
3288
+ "soccer",
3289
+ "worldcup",
3290
+ "world-cup-2026",
3291
+ "cli",
3292
+ "terminal",
3293
+ "livescore",
3294
+ "live-commentary",
3295
+ "tui"
3296
+ ],
3297
+ author: "juunghyun",
3298
+ license: "MIT",
3299
+ devDependencies: {
3300
+ "@types/node": "^20.14.0",
3301
+ "env-paths": "^3.0.0",
3302
+ "string-width": "^7.2.0",
3303
+ tsup: "^8.1.0",
3304
+ typescript: "^5.5.0",
3305
+ vitest: "^2.0.0"
3306
+ }
3307
+ };
3308
+
3309
+ // src/cli/index.ts
3310
+ function parseArgv(argv) {
3311
+ const flags = { anim: true, record: true };
3312
+ const rest = [];
3313
+ for (let i = 0; i < argv.length; i++) {
3314
+ const a = argv[i];
3315
+ if (a === "--lang") {
3316
+ const v = argv[++i];
3317
+ if (v === "ko" || v === "en") flags.lang = v;
3318
+ } else if (a.startsWith("--lang=")) {
3319
+ const v = a.slice(7);
3320
+ if (v === "ko" || v === "en") flags.lang = v;
3321
+ } else if (a === "--speed") {
3322
+ flags.speed = Number(argv[++i]);
3323
+ } else if (a.startsWith("--speed=")) {
3324
+ flags.speed = Number(a.slice(8));
3325
+ } else if (a === "--no-anim") flags.anim = false;
3326
+ else if (a === "--no-record") flags.record = false;
3327
+ else if (a === "--version" || a === "-v") {
3328
+ console.log(package_default.version);
3329
+ process.exit(0);
3330
+ } else if (a === "--help" || a === "-h") rest.unshift("help");
3331
+ else rest.push(a);
3332
+ }
3333
+ return { cmd: rest[0], args: rest.slice(1), flags };
3334
+ }
3335
+ var HELP = `
3336
+ ${bold("termfc")} \u2014 FIFA World Cup 2026 live text commentary in your terminal
3337
+
3338
+ ${bold("usage")}
3339
+ termfc ${dim("live + upcoming matches, pick one to join")}
3340
+ termfc live ${dim("matches in progress")}
3341
+ termfc schedule ${dim("full schedule window")}
3342
+ termfc watch <team|id> ${dim("join a match (e.g. termfc watch KOR)")}
3343
+ termfc replay [file] ${dim("replay a recorded match (no arg: list recordings)")}
3344
+ termfc demo ${dim("bundled demo match (no network needed)")}
3345
+
3346
+ ${bold("options")}
3347
+ --lang ko|en ${dim("commentary language (default: ko, persisted)")}
3348
+ --speed N ${dim("replay/demo speed multiplier")}
3349
+ --no-anim ${dim("skip entrance/goal animations")}
3350
+ --no-record ${dim("don't record watched matches for replay")}
3351
+ `;
3352
+ async function main() {
3353
+ const { cmd, args, flags } = parseArgv(process.argv.slice(2));
3354
+ const config = readConfig();
3355
+ const lang = flags.lang ?? config.lang ?? "ko";
3356
+ if (flags.lang && flags.lang !== config.lang) {
3357
+ try {
3358
+ writeConfig({ ...config, lang: flags.lang });
3359
+ } catch {
3360
+ }
3361
+ }
3362
+ const l = labels(lang);
3363
+ if (cmd === "help") {
3364
+ console.log(renderBanner(package_default.version));
3365
+ console.log(HELP);
3366
+ return;
3367
+ }
3368
+ console.log(renderBanner(package_default.version));
3369
+ const fifa = new FifaProvider();
3370
+ const espn = new EspnProvider();
3371
+ const provider = new FailoverProvider(fifa, espn);
3372
+ if (cmd === "demo") {
3373
+ await runReplay(demo_match_default, lang, flags, 120);
3374
+ return;
3375
+ }
3376
+ if (cmd === "replay") {
3377
+ await replayCommand(args[0], lang, flags);
3378
+ return;
3379
+ }
3380
+ const { matches, staleAt } = await loadSchedule(provider, espn, lang);
3381
+ if (matches.length === 0) {
3382
+ console.log(red(` ${l.noData}
3383
+ `));
3384
+ process.exitCode = 1;
3385
+ return;
3386
+ }
3387
+ if (cmd === "schedule") {
3388
+ renderList(matches, { lang, staleAt });
3389
+ return;
3390
+ }
3391
+ if (cmd === "live") {
3392
+ const live = matches.filter((m) => isLivePhase(m.phase));
3393
+ const pickable2 = renderList(
3394
+ live.length > 0 ? live : matches.filter((m) => m.phase === "SCHEDULED").slice(0, 5),
3395
+ { lang, staleAt }
3396
+ );
3397
+ if (live.length === 0) return;
3398
+ const choice2 = await pickMatch(pickable2, lang);
3399
+ if (choice2) await watch(choice2, provider, lang, flags);
3400
+ return;
3401
+ }
3402
+ if (cmd === "watch") {
3403
+ const ident = args[0];
3404
+ if (!ident) {
3405
+ console.log(HELP);
3406
+ return;
3407
+ }
3408
+ const match = findMatch(matches, ident);
3409
+ if (!match) {
3410
+ console.log(red(` match not found: ${ident}
3411
+ `));
3412
+ process.exitCode = 1;
3413
+ return;
3414
+ }
3415
+ await watch(match, provider, lang, flags);
3416
+ return;
3417
+ }
3418
+ if (cmd !== void 0) {
3419
+ console.log(HELP);
3420
+ return;
3421
+ }
3422
+ const pickable = renderList(matches, { lang, staleAt });
3423
+ const choice = await pickMatch(pickable, lang);
3424
+ if (choice) await watch(choice, provider, lang, flags);
3425
+ }
3426
+ async function loadSchedule(provider, espn, lang) {
3427
+ const cached = readScheduleCache(lang);
3428
+ if (cached && isScheduleFresh(cached)) {
3429
+ return { matches: cached.matches, staleAt: null };
3430
+ }
3431
+ try {
3432
+ const matches = await provider.fetchSchedule(lang);
3433
+ if (provider.activeSource === "fifa") {
3434
+ try {
3435
+ linkSourceRefs(matches, await espn.fetchSchedule(lang));
3436
+ } catch {
3437
+ }
3438
+ }
3439
+ writeScheduleCache(matches, lang);
3440
+ return { matches, staleAt: null };
3441
+ } catch {
3442
+ if (cached) return { matches: cached.matches, staleAt: cached.fetchedAt };
3443
+ return { matches: [], staleAt: null };
3444
+ }
3445
+ }
3446
+ function findMatch(matches, ident) {
3447
+ const byId = matches.find((m) => m.id === ident);
3448
+ if (byId) return byId;
3449
+ const q = ident.toUpperCase();
3450
+ const hits = matches.filter(
3451
+ (m) => m.home.code === q || m.away.code === q || m.home.name.toUpperCase().includes(q) || m.away.name.toUpperCase().includes(q)
3452
+ );
3453
+ if (hits.length === 0) return null;
3454
+ return hits.find((m) => isLivePhase(m.phase)) ?? hits.find((m) => m.phase === "SCHEDULED") ?? hits[hits.length - 1] ?? null;
3455
+ }
3456
+ async function watch(match, provider, lang, flags) {
3457
+ const lambdas = await fetchPreMatchLambdas(match);
3458
+ const engine = new PollingEngine(provider, match, lang);
3459
+ let recorder = null;
3460
+ if (flags.record) {
3461
+ try {
3462
+ recorder = new ReplayRecorder(match);
3463
+ } catch {
3464
+ recorder = null;
3465
+ }
3466
+ }
3467
+ const screen = new MatchScreen(match, engine, {
3468
+ lang,
3469
+ mode: "live",
3470
+ animations: flags.anim,
3471
+ lambdas,
3472
+ sourceLabel: () => provider.activeSource.toUpperCase(),
3473
+ onEvents: (events) => recorder?.append(events)
3474
+ });
3475
+ await screen.run();
3476
+ }
3477
+ async function replayCommand(arg, lang, flags) {
3478
+ const l = labels(lang);
3479
+ let file = arg;
3480
+ if (!file) {
3481
+ const files = existsSync3(REPLAY_DIR) ? readdirSync(REPLAY_DIR).filter((f) => f.endsWith(".jsonl")) : [];
3482
+ if (files.length === 0) {
3483
+ console.log(dim(` (${l.recorded} 0) \u2192 ${cyan("termfc demo")}
3484
+ `));
3485
+ return;
3486
+ }
3487
+ console.log(` ${bold(l.replaying)}
3488
+ `);
3489
+ files.forEach(
3490
+ (f, i) => console.log(` ${dim(`${String(i + 1).padStart(2)}.`)} ${f}`)
3491
+ );
3492
+ console.log();
3493
+ const { createInterface: createInterface2 } = await import("readline/promises");
3494
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3495
+ const answer = (await rl.question(` ${l.pickPrompt}`)).trim();
3496
+ rl.close();
3497
+ const n = Number(answer);
3498
+ if (!Number.isInteger(n) || n < 1 || n > files.length) return;
3499
+ file = join3(REPLAY_DIR, files[n - 1]);
3500
+ } else if (!existsSync3(file) && existsSync3(join3(REPLAY_DIR, file))) {
3501
+ file = join3(REPLAY_DIR, file);
3502
+ }
3503
+ if (!existsSync3(file)) {
3504
+ console.log(red(` file not found: ${file}
3505
+ `));
3506
+ process.exitCode = 1;
3507
+ return;
3508
+ }
3509
+ const parsed = parseReplay(readFileSync2(file, "utf8"));
3510
+ await runReplay(parsed, lang, flags, 60);
3511
+ }
3512
+ async function runReplay(parsed, lang, flags, defaultSpeed) {
3513
+ const speed = flags.speed && Number.isFinite(flags.speed) && flags.speed > 0 ? flags.speed : defaultSpeed;
3514
+ const feed = new ReplayFeed(parsed, speed);
3515
+ const screen = new MatchScreen(parsed.match, feed, {
3516
+ lang,
3517
+ mode: "replay",
3518
+ animations: flags.anim,
3519
+ clockRate: speed,
3520
+ speedLabel: String(speed),
3521
+ lambdas: null,
3522
+ sourceLabel: () => `${parsed.events[0]?.source ?? "fifa"} replay`
3523
+ });
3524
+ await screen.run();
3525
+ }
3526
+ main().then(() => process.exit(process.exitCode ?? 0)).catch((err) => {
3527
+ console.error(red(`
3528
+ termfc error: ${err?.message ?? err}
3529
+ `));
3530
+ console.error(
3531
+ yellow(" data sources may be unavailable \u2014 try again shortly\n")
3532
+ );
3533
+ process.exit(1);
3534
+ });