soccer-cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/dist/index.js +401 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,20 +3,27 @@
|
|
|
3
3
|
Live football scores, **ball-by-ball commentary**, and a **live ASCII pitch** — right in your terminal. Built for the 2026 World Cup, works for every major league. **No API key required.**
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
⚽ FIFA WORLD CUP 2026
|
|
6
|
+
⚽ FIFA WORLD CUP 2026 ● 1 LIVE 3 matches · ⟳15s
|
|
7
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
7
8
|
MATCHES
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
▸ 🇫🇷 France 2-1 England 🏴 ● 67' ← selected row is highlighted
|
|
10
|
+
🇧🇷 Brazil v Germany 🇩🇪 Thu 8:00 PM
|
|
11
|
+
🇦🇷 Argentina 3-0 Japan 🇯🇵 FT
|
|
12
|
+
|
|
13
|
+
🇫🇷 France ● 67' England 🏴
|
|
14
|
+
▀▀█ ▀█
|
|
15
|
+
█▀▀ ▀▀▀ █ ← big block-digit score
|
|
16
|
+
▀▀▀ ▀
|
|
17
|
+
⚽ 23' Mbappé (P) · 38' Bellingham · 66' Mbappé
|
|
18
|
+
0' ────────⚽──────⚽───┃───▮──────⚽──────── 90' ← match timeline
|
|
19
|
+
Possession 58% ███████████████████░░░░░░░░░░░░░░ 42%
|
|
20
|
+
Shots 14 ████████████████▏██████████▎░░░░░ 9 ← duel bars
|
|
14
21
|
╭────────────────────────────────────────────────────────╮
|
|
15
22
|
│ ·····┆ ★ ┆ ┆····· │
|
|
16
|
-
│▐ ★ ┆ ● ( • ) ┆ ▌│
|
|
23
|
+
│▐ ★ ┆ ● ( • ) ┆ ▌│ ← striped-grass pitch
|
|
17
24
|
│ ·····┆ ┆ ┆····· │
|
|
18
25
|
╰────────────────────────────────────────────────────────╯
|
|
19
|
-
●
|
|
26
|
+
● France → ← England ● 66' ★ GOAL
|
|
20
27
|
```
|
|
21
28
|
|
|
22
29
|
## Install
|
|
@@ -59,11 +66,14 @@ Any ESPN soccer slug works — a few common ones:
|
|
|
59
66
|
|
|
60
67
|
## What you get
|
|
61
68
|
|
|
62
|
-
- **Live scores** that auto-refresh
|
|
63
|
-
- **
|
|
64
|
-
- **A
|
|
65
|
-
- **
|
|
69
|
+
- **Live scores** that auto-refresh, with a **pulsing live indicator** and a **goal flash** the moment anyone scores
|
|
70
|
+
- **A big block-digit scoreboard** with a scorers line (`⚽ 23' Mbappé (P) · 66' Kane`)
|
|
71
|
+
- **A match timeline** — goals, cards and missed penalties plotted from kickoff to full time
|
|
72
|
+
- **Visual stat bars** — possession as a team-colored split bar, shots / on-target / corners as mirrored duel bars
|
|
73
|
+
- **Ball-by-ball commentary** in a clean, word-wrapped table with color-coded event markers
|
|
74
|
+
- **A live pitch** with mowed-grass stripes, plotting where the action is — goals (★), the latest play (●), and a fading trail of recent events
|
|
66
75
|
- **Country flags** for every nation
|
|
76
|
+
- **256-color visuals** that degrade gracefully — plain 16-color terminals, `NO_COLOR`, and piped output all stay clean
|
|
67
77
|
|
|
68
78
|
## Data
|
|
69
79
|
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/index.ts
|
|
4
|
-
import
|
|
4
|
+
import process3 from "process";
|
|
5
5
|
|
|
6
6
|
// src/app.ts
|
|
7
7
|
import logUpdate from "log-update";
|
|
8
8
|
import readline from "readline";
|
|
9
|
-
import
|
|
9
|
+
import process2 from "process";
|
|
10
10
|
|
|
11
11
|
// src/flags.ts
|
|
12
12
|
import { createRequire } from "module";
|
|
@@ -327,10 +327,11 @@ async function fetchSummary(league2, eventId, signal) {
|
|
|
327
327
|
// src/render.ts
|
|
328
328
|
import Table from "cli-table3";
|
|
329
329
|
import pc3 from "picocolors";
|
|
330
|
-
import
|
|
330
|
+
import stringWidth3 from "string-width";
|
|
331
331
|
|
|
332
332
|
// src/theme.ts
|
|
333
333
|
import pc from "picocolors";
|
|
334
|
+
import process from "process";
|
|
334
335
|
var SYM = {
|
|
335
336
|
live: "\u25CF",
|
|
336
337
|
goal: "\u26BD",
|
|
@@ -347,6 +348,74 @@ var SYM = {
|
|
|
347
348
|
ball: "\u26BD",
|
|
348
349
|
sel: "\u25B8"
|
|
349
350
|
};
|
|
351
|
+
var supports256 = pc.isColorSupported && (!!process.env.COLORTERM || /256color|truecolor/i.test(process.env.TERM ?? ""));
|
|
352
|
+
function ansi256(n) {
|
|
353
|
+
return (s) => supports256 ? `\x1B[38;5;${n}m${s}\x1B[39m` : s;
|
|
354
|
+
}
|
|
355
|
+
function bg256(n) {
|
|
356
|
+
return (s) => supports256 ? `\x1B[48;5;${n}m${s}\x1B[49m` : s;
|
|
357
|
+
}
|
|
358
|
+
function fg(n, fallback) {
|
|
359
|
+
return supports256 ? ansi256(n) : fallback;
|
|
360
|
+
}
|
|
361
|
+
var HOME_C = fg(45, pc.cyan);
|
|
362
|
+
var AWAY_C = fg(213, pc.magenta);
|
|
363
|
+
var GRAD_TITLE = [201, 200, 199, 198, 197, 203, 209, 215];
|
|
364
|
+
var GRAD_RULE = [54, 92, 128, 164, 200, 206, 212, 218];
|
|
365
|
+
function gradient(text, palette) {
|
|
366
|
+
if (!supports256 || palette.length === 0) return c.title(text);
|
|
367
|
+
const chars = [...text];
|
|
368
|
+
let out = "";
|
|
369
|
+
for (let i = 0; i < chars.length; i++) {
|
|
370
|
+
const ch = chars[i];
|
|
371
|
+
if (ch === " ") {
|
|
372
|
+
out += ch;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const n = palette[Math.min(palette.length - 1, Math.floor(i / chars.length * palette.length))];
|
|
376
|
+
out += `\x1B[38;5;${n}m${ch}`;
|
|
377
|
+
}
|
|
378
|
+
return `\x1B[1m${out}\x1B[22m\x1B[39m`;
|
|
379
|
+
}
|
|
380
|
+
function badge(text, bgN, fallback) {
|
|
381
|
+
const t = ` ${text} `;
|
|
382
|
+
if (supports256) return `\x1B[48;5;${bgN}m\x1B[38;5;231m\x1B[1m${t}\x1B[22m\x1B[39m\x1B[49m`;
|
|
383
|
+
return pc.inverse(fallback(t));
|
|
384
|
+
}
|
|
385
|
+
function splitBar(left, right, width) {
|
|
386
|
+
const total = left + right;
|
|
387
|
+
if (total <= 0 || width <= 0) return pc.gray("\u2591".repeat(Math.max(0, width)));
|
|
388
|
+
const lw = Math.max(0, Math.min(width, Math.round(left / total * width)));
|
|
389
|
+
if (!pc.isColorSupported) return "\u2588".repeat(lw) + "\u2591".repeat(width - lw);
|
|
390
|
+
return HOME_C("\u2588".repeat(lw)) + AWAY_C("\u2588".repeat(width - lw));
|
|
391
|
+
}
|
|
392
|
+
var EIGHTHS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
|
|
393
|
+
function miniBar(value, max, width, color) {
|
|
394
|
+
if (width <= 0) return "";
|
|
395
|
+
const ratio = max > 0 ? Math.max(0, Math.min(1, value / max)) : 0;
|
|
396
|
+
const cells = ratio * width;
|
|
397
|
+
const full = Math.floor(cells);
|
|
398
|
+
const frac = EIGHTHS[Math.round((cells - full) * 7)];
|
|
399
|
+
const bar = "\u2588".repeat(full) + frac;
|
|
400
|
+
const pad = width - full - (frac ? 1 : 0);
|
|
401
|
+
return color(bar) + pc.gray("\u2591".repeat(Math.max(0, pad)));
|
|
402
|
+
}
|
|
403
|
+
function miniBarRev(value, max, width, color) {
|
|
404
|
+
if (width <= 0) return "";
|
|
405
|
+
const ratio = max > 0 ? Math.max(0, Math.min(1, value / max)) : 0;
|
|
406
|
+
const cells = ratio * width;
|
|
407
|
+
const full = Math.floor(cells);
|
|
408
|
+
const frac = cells - full >= 0.5 ? "\u2590" : "";
|
|
409
|
+
const pad = width - full - (frac ? 1 : 0);
|
|
410
|
+
return pc.gray("\u2591".repeat(Math.max(0, pad))) + color(frac + "\u2588".repeat(full));
|
|
411
|
+
}
|
|
412
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
413
|
+
function spinner(tick) {
|
|
414
|
+
return SPINNER[tick % SPINNER.length];
|
|
415
|
+
}
|
|
416
|
+
function pulse(tick) {
|
|
417
|
+
return tick % 2 === 0 ? "\u25CF" : "\u25CB";
|
|
418
|
+
}
|
|
350
419
|
var c = {
|
|
351
420
|
title: (s) => pc.bold(pc.magenta(s)),
|
|
352
421
|
live: (s) => pc.green(s),
|
|
@@ -372,11 +441,31 @@ function commentaryColor(text, isGoal) {
|
|
|
372
441
|
if (t.includes("substitution") || t.includes("replaces")) return pc.cyan;
|
|
373
442
|
return (s) => s;
|
|
374
443
|
}
|
|
444
|
+
function commentaryMark(text, isGoal) {
|
|
445
|
+
const t = text.toLowerCase();
|
|
446
|
+
if (isGoal) return pc.bold(pc.green("\u258C"));
|
|
447
|
+
if (t.includes("red card")) return pc.bold(pc.red("\u258C"));
|
|
448
|
+
if (t.includes("yellow card") || t.includes("booked")) return pc.yellow("\u258C");
|
|
449
|
+
if (t.includes("penalty")) return c.accent("\u258C");
|
|
450
|
+
if (t.includes("substitution") || t.includes("replaces")) return pc.cyan("\u258C");
|
|
451
|
+
if (t.includes("var")) return fg(141, pc.magenta)("\u258C");
|
|
452
|
+
return pc.gray("\u258F");
|
|
453
|
+
}
|
|
375
454
|
|
|
376
455
|
// src/format.ts
|
|
456
|
+
import stringWidth from "string-width";
|
|
377
457
|
function truncate(s, max) {
|
|
378
|
-
if (max <=
|
|
379
|
-
|
|
458
|
+
if (max <= 0) return "";
|
|
459
|
+
if (stringWidth(s) <= max) return s;
|
|
460
|
+
let out = "";
|
|
461
|
+
let w = 0;
|
|
462
|
+
for (const ch of s) {
|
|
463
|
+
const cw = stringWidth(ch);
|
|
464
|
+
if (w + cw > max - 1) break;
|
|
465
|
+
out += ch;
|
|
466
|
+
w += cw;
|
|
467
|
+
}
|
|
468
|
+
return out + "\u2026";
|
|
380
469
|
}
|
|
381
470
|
function kickoffLocal(iso) {
|
|
382
471
|
if (!iso) return "";
|
|
@@ -391,10 +480,11 @@ function kickoffLocal(iso) {
|
|
|
391
480
|
|
|
392
481
|
// src/pitch.ts
|
|
393
482
|
import pc2 from "picocolors";
|
|
394
|
-
import
|
|
395
|
-
var HOME = pc2.cyan;
|
|
396
|
-
var AWAY = pc2.magenta;
|
|
483
|
+
import stringWidth2 from "string-width";
|
|
397
484
|
var LINE = (s) => pc2.gray(s);
|
|
485
|
+
var STRIPE_A = "\x1B[48;5;22m";
|
|
486
|
+
var STRIPE_B = "\x1B[48;5;28m";
|
|
487
|
+
var BG_OFF = "\x1B[49m";
|
|
398
488
|
function mapEvent(x, y, isHome, IW, IH) {
|
|
399
489
|
const xAbs = isHome ? 1 - x : x;
|
|
400
490
|
const yAbs = isHome ? y : 1 - y;
|
|
@@ -434,8 +524,8 @@ function renderPitch(detail, innerWidth) {
|
|
|
434
524
|
}
|
|
435
525
|
for (let r = rmid - 1; r <= rmid + 1; r++) {
|
|
436
526
|
if (r < 0 || r >= IH) continue;
|
|
437
|
-
grid[r][0] =
|
|
438
|
-
grid[r][IW - 1] =
|
|
527
|
+
grid[r][0] = HOME_C("\u2590");
|
|
528
|
+
grid[r][IW - 1] = AWAY_C("\u258C");
|
|
439
529
|
}
|
|
440
530
|
const events = detail.commentary.filter((c2) => c2.x != null && c2.y != null);
|
|
441
531
|
const homeId = detail.home.id;
|
|
@@ -446,26 +536,67 @@ function renderPitch(detail, innerWidth) {
|
|
|
446
536
|
const recent = events.slice(0, 7);
|
|
447
537
|
for (let i = recent.length - 1; i >= 1; i--) {
|
|
448
538
|
const e = recent[i];
|
|
449
|
-
plot(e, (e.teamId === homeId ?
|
|
539
|
+
plot(e, (e.teamId === homeId ? HOME_C : AWAY_C)(pc2.dim("\u2219")));
|
|
450
540
|
}
|
|
451
541
|
for (const g of events.filter((e) => e.isGoal)) plot(g, pc2.bold(pc2.green("\u2605")));
|
|
452
542
|
const newest = recent[0];
|
|
453
543
|
if (newest) {
|
|
454
|
-
plot(newest, newest.isGoal ? pc2.bold(pc2.green("\u2605")) : pc2.bold((newest.teamId === homeId ?
|
|
544
|
+
plot(newest, newest.isGoal ? pc2.bold(pc2.green("\u2605")) : pc2.bold((newest.teamId === homeId ? HOME_C : AWAY_C)("\u25CF")));
|
|
455
545
|
}
|
|
546
|
+
const joinRow = (r) => {
|
|
547
|
+
if (!supports256) return r.join("");
|
|
548
|
+
let out = "";
|
|
549
|
+
for (let i = 0; i < r.length; i++) {
|
|
550
|
+
out += (Math.floor(i / 5) % 2 === 0 ? STRIPE_A : STRIPE_B) + r[i];
|
|
551
|
+
}
|
|
552
|
+
return out + BG_OFF;
|
|
553
|
+
};
|
|
456
554
|
const top = LINE(` \u256D${"\u2500".repeat(IW)}\u256E`);
|
|
457
555
|
const bot = LINE(` \u2570${"\u2500".repeat(IW)}\u256F`);
|
|
458
|
-
const body = grid.map((r) => `${LINE(" \u2502")}${r
|
|
556
|
+
const body = grid.map((r) => `${LINE(" \u2502")}${joinRow(r)}${LINE("\u2502")}`);
|
|
459
557
|
const latest = events[0];
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
558
|
+
const buildLegend = (nameW, withTag) => {
|
|
559
|
+
let l = ` ${HOME_C("\u25CF")} ${detail.home.flag} ${pc2.bold(truncate(detail.home.name, nameW))} ${pc2.gray("\u2192")} ${pc2.gray("\u2190")} ${pc2.bold(truncate(detail.away.name, nameW))} ${detail.away.flag} ${AWAY_C("\u25CF")}`;
|
|
560
|
+
if (latest && withTag) {
|
|
561
|
+
const tag = latest.isGoal ? pc2.bold(pc2.green("\u2605 GOAL")) : pc2.gray(latest.playType || "ball");
|
|
562
|
+
l += ` ${pc2.gray(latest.time)} ${tag}`;
|
|
563
|
+
}
|
|
564
|
+
return l;
|
|
565
|
+
};
|
|
566
|
+
let legend = buildLegend(16, true);
|
|
567
|
+
if (stringWidth2(legend) > IW + 4) legend = buildLegend(16, false);
|
|
568
|
+
if (stringWidth2(legend) > IW + 4) legend = buildLegend(10, false);
|
|
466
569
|
return [top, ...body, bot, legend].join("\n");
|
|
467
570
|
}
|
|
468
571
|
|
|
572
|
+
// src/digits.ts
|
|
573
|
+
var FONT = {
|
|
574
|
+
"0": ["\u2588\u2580\u2588", "\u2588 \u2588", "\u2580\u2580\u2580"],
|
|
575
|
+
"1": [" \u2580\u2588", " \u2588", " \u2580"],
|
|
576
|
+
"2": ["\u2580\u2580\u2588", "\u2588\u2580\u2580", "\u2580\u2580\u2580"],
|
|
577
|
+
"3": ["\u2580\u2580\u2588", " \u2580\u2588", "\u2580\u2580\u2580"],
|
|
578
|
+
"4": ["\u2588 \u2588", "\u2580\u2580\u2588", " \u2580"],
|
|
579
|
+
"5": ["\u2588\u2580\u2580", "\u2580\u2580\u2588", "\u2580\u2580\u2580"],
|
|
580
|
+
"6": ["\u2588\u2580\u2580", "\u2588\u2580\u2588", "\u2580\u2580\u2580"],
|
|
581
|
+
"7": ["\u2580\u2580\u2588", " \u2588", " \u2580"],
|
|
582
|
+
"8": ["\u2588\u2580\u2588", "\u2588\u2580\u2588", "\u2580\u2580\u2580"],
|
|
583
|
+
"9": ["\u2588\u2580\u2588", "\u2580\u2580\u2588", "\u2580\u2580\u2580"],
|
|
584
|
+
"-": [" ", "\u2580\u2580\u2580", " "],
|
|
585
|
+
"\u2013": [" ", "\u2580\u2580\u2580", " "],
|
|
586
|
+
":": [" \u2580 ", " \u2580 ", " "],
|
|
587
|
+
" ": [" ", " ", " "]
|
|
588
|
+
};
|
|
589
|
+
function bigDigits(text) {
|
|
590
|
+
const rows = [[], [], []];
|
|
591
|
+
for (const ch of text) {
|
|
592
|
+
const g = FONT[ch] ?? FONT[" "];
|
|
593
|
+
rows[0].push(g[0]);
|
|
594
|
+
rows[1].push(g[1]);
|
|
595
|
+
rows[2].push(g[2]);
|
|
596
|
+
}
|
|
597
|
+
return [rows[0].join(" "), rows[1].join(" "), rows[2].join(" ")];
|
|
598
|
+
}
|
|
599
|
+
|
|
469
600
|
// src/render.ts
|
|
470
601
|
var CHARS = {
|
|
471
602
|
top: "\u2500",
|
|
@@ -484,6 +615,7 @@ var CHARS = {
|
|
|
484
615
|
"right-mid": "\u2524",
|
|
485
616
|
middle: "\u2502"
|
|
486
617
|
};
|
|
618
|
+
var liveClock = fg(46, pc3.green);
|
|
487
619
|
function makeTable(opts) {
|
|
488
620
|
return new Table({
|
|
489
621
|
chars: CHARS,
|
|
@@ -492,82 +624,207 @@ function makeTable(opts) {
|
|
|
492
624
|
});
|
|
493
625
|
}
|
|
494
626
|
function padEndW(s, w) {
|
|
495
|
-
const d = w -
|
|
627
|
+
const d = w - stringWidth3(s);
|
|
496
628
|
return d > 0 ? s + " ".repeat(d) : s;
|
|
497
629
|
}
|
|
498
630
|
function padCenterW(s, w) {
|
|
499
|
-
const d = Math.max(0, w -
|
|
631
|
+
const d = Math.max(0, w - stringWidth3(s));
|
|
500
632
|
const left = Math.floor(d / 2);
|
|
501
633
|
return " ".repeat(left) + s + " ".repeat(d - left);
|
|
502
634
|
}
|
|
503
635
|
function headerLine(s, width) {
|
|
504
|
-
const left =
|
|
636
|
+
const left = `${SYM.ball} ${gradient(s.title, GRAD_TITLE)}`;
|
|
505
637
|
const live = s.matches.filter((m) => m.state === "in").length;
|
|
506
|
-
const liveTxt = live > 0 ? `${
|
|
638
|
+
const liveTxt = live > 0 ? `${badge(`${SYM.live} ${live} LIVE`, 196, pc3.red)} ` : "";
|
|
507
639
|
const upd = s.updatedAt ? new Date(s.updatedAt).toLocaleTimeString() : "\u2014";
|
|
508
|
-
const
|
|
509
|
-
const right = `${liveTxt}${pc3.gray(`${s.matches.length} matches \xB7 ${upd}${
|
|
510
|
-
const pad = Math.max(1, width -
|
|
640
|
+
const cdTxt = s.countdown == null ? "" : s.countdown <= 3 ? ` ${SYM.refresh}${pc3.bold(pc3.white(`${s.countdown}s`))}` : ` ${SYM.refresh}${s.countdown}s`;
|
|
641
|
+
const right = `${liveTxt}${pc3.gray(`${s.matches.length} matches \xB7 ${upd}${cdTxt}`)}`;
|
|
642
|
+
const pad = Math.max(1, width - stringWidth3(left) - stringWidth3(right));
|
|
511
643
|
return `${left}${" ".repeat(pad)}${right}`;
|
|
512
644
|
}
|
|
645
|
+
function headerRule(width) {
|
|
646
|
+
const w = Math.max(10, Math.min(width, 118));
|
|
647
|
+
if (!supports256) return pc3.gray("\u2501".repeat(w));
|
|
648
|
+
return gradient("\u2501".repeat(w), GRAD_RULE);
|
|
649
|
+
}
|
|
513
650
|
function fixturesList(s, width) {
|
|
514
651
|
const NAME = 14;
|
|
515
652
|
const COLW = NAME + 3;
|
|
516
|
-
const lines = [` ${pc3.bold(pc3.gray("MATCHES"))}
|
|
653
|
+
const lines = [` ${pc3.bold(pc3.gray("MATCHES"))}`];
|
|
517
654
|
s.matches.forEach((m, i) => {
|
|
518
655
|
const sel = i === s.selected;
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
|
|
656
|
+
const post = m.state === "post";
|
|
657
|
+
const hs = m.home.score ?? 0;
|
|
658
|
+
const as = m.away.score ?? 0;
|
|
659
|
+
let hn = truncate(m.home.name, NAME);
|
|
660
|
+
let an = truncate(m.away.name, NAME);
|
|
661
|
+
if (post && hs !== as) {
|
|
662
|
+
if (hs > as) hn = pc3.bold(hn);
|
|
663
|
+
else an = pc3.bold(an);
|
|
664
|
+
}
|
|
665
|
+
const home = padEndW(`${m.home.flag} ${hn}`, COLW);
|
|
666
|
+
const away = padEndW(`${an} ${m.away.flag}`, COLW);
|
|
667
|
+
const flashing = s.flashIds.includes(m.id) && s.tick % 2 === 0;
|
|
668
|
+
let score;
|
|
669
|
+
if (m.state === "pre") {
|
|
670
|
+
score = padCenterW(pc3.gray("v"), 5);
|
|
671
|
+
} else if (flashing) {
|
|
672
|
+
score = padCenterW(pc3.inverse(c.goal(` ${hs}-${as} `)), 5);
|
|
673
|
+
} else {
|
|
674
|
+
const ch = hs >= as ? c.score : pc3.gray;
|
|
675
|
+
const ca = as >= hs ? c.score : pc3.gray;
|
|
676
|
+
score = padCenterW(`${ch(String(hs))}${pc3.gray("-")}${ca(String(as))}`, 5);
|
|
677
|
+
}
|
|
678
|
+
const status = m.state === "in" ? `${c.live(pulse(s.tick))} ${pc3.bold(liveClock(m.clock || "LIVE"))}` : post ? c.finished(m.shortDetail || "FT") : c.upcoming(kickoffLocal(m.date) || "Scheduled");
|
|
523
679
|
const arrow = sel ? c.sel(SYM.sel) : " ";
|
|
524
|
-
|
|
680
|
+
let row = ` ${arrow} ${home} ${score} ${away} ${status}`;
|
|
681
|
+
if (post && !sel) row = pc3.dim(row);
|
|
682
|
+
if (sel && supports256) row = bg256(236)(padEndW(row, width));
|
|
683
|
+
else if (sel) row = ` ${c.sel(SYM.sel)} ${c.sel(home)} ${score} ${c.sel(away)} ${status}`;
|
|
525
684
|
lines.push(row);
|
|
526
685
|
});
|
|
527
686
|
return lines.join("\n");
|
|
528
687
|
}
|
|
529
|
-
function
|
|
688
|
+
function scoreboard(s, m) {
|
|
530
689
|
const d = s.detail && s.detail.id === m.id ? s.detail : null;
|
|
531
690
|
const home = d?.home ?? m.home;
|
|
532
691
|
const away = d?.away ?? m.away;
|
|
533
692
|
const state = d?.state ?? m.state;
|
|
534
693
|
const clock = d?.clock || m.clock;
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
694
|
+
const W = Math.min(s.cols - 2, 118);
|
|
695
|
+
const badgeTxt = state === "in" ? badge(`${pulse(s.tick)} ${clock || "LIVE"}`, 28, pc3.green) : state === "post" ? badge(d?.shortDetail || m.shortDetail || "FT", 240, pc3.cyan) : c.upcoming(kickoffLocal(m.date) || "Scheduled");
|
|
696
|
+
const left = ` ${home.flag} ${pc3.bold(HOME_C(truncate(home.name, 24)))}`;
|
|
697
|
+
const right = `${pc3.bold(AWAY_C(truncate(away.name, 24)))} ${away.flag} `;
|
|
698
|
+
const gap = Math.max(2, W - stringWidth3(left) - stringWidth3(badgeTxt) - stringWidth3(right));
|
|
699
|
+
const g1 = Math.floor(gap / 2);
|
|
700
|
+
const nameLine = `${left}${" ".repeat(g1)}${badgeTxt}${" ".repeat(gap - g1)}${right}`;
|
|
701
|
+
const lines = [nameLine];
|
|
702
|
+
if (state !== "pre") {
|
|
703
|
+
const flashing = s.flashIds.includes(m.id) && s.tick % 2 === 0;
|
|
704
|
+
const colorScore = flashing ? (t) => pc3.bold(pc3.green(t)) : c.score;
|
|
705
|
+
const rows = bigDigits(`${home.score ?? "-"}-${away.score ?? "-"}`);
|
|
706
|
+
rows.forEach((r, i) => {
|
|
707
|
+
let line = padCenterW(colorScore(r), W);
|
|
708
|
+
if (i === 1 && s.flashIds.includes(m.id)) line = line.trimEnd();
|
|
709
|
+
lines.push(line);
|
|
710
|
+
});
|
|
711
|
+
if (s.flashIds.includes(m.id)) {
|
|
712
|
+
const mid = lines.length - 2;
|
|
713
|
+
lines[mid] = `${lines[mid]} ${badge("GOAL!", 28, pc3.green)}`;
|
|
714
|
+
}
|
|
715
|
+
const scorers = scorersLine(d, home.id, W);
|
|
716
|
+
if (scorers) lines.push(scorers);
|
|
553
717
|
}
|
|
554
|
-
return
|
|
718
|
+
return lines;
|
|
719
|
+
}
|
|
720
|
+
function scorersLine(d, homeId, width) {
|
|
721
|
+
if (!d) return "";
|
|
722
|
+
const goals = d.keyEvents.filter((k) => ["goal", "penalty-goal", "own-goal"].includes(k.kind));
|
|
723
|
+
if (goals.length === 0) return "";
|
|
724
|
+
let line = ` ${SYM.goal} `;
|
|
725
|
+
let used = stringWidth3(line);
|
|
726
|
+
let added = 0;
|
|
727
|
+
for (const g of goals) {
|
|
728
|
+
const tag = g.kind === "penalty-goal" ? " (P)" : g.kind === "own-goal" ? " (OG)" : "";
|
|
729
|
+
const plain = `${g.clock} ${g.player ?? g.team ?? "?"}${tag}`;
|
|
730
|
+
const sepW = added > 0 ? 3 : 0;
|
|
731
|
+
if (used + sepW + stringWidth3(plain) > width - 2) {
|
|
732
|
+
line += pc3.gray(" \u2026");
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
const color = g.teamId === homeId ? HOME_C : AWAY_C;
|
|
736
|
+
line += (added > 0 ? pc3.gray(" \xB7 ") : "") + color(plain);
|
|
737
|
+
used += sepW + stringWidth3(plain);
|
|
738
|
+
added += 1;
|
|
739
|
+
}
|
|
740
|
+
return line;
|
|
741
|
+
}
|
|
742
|
+
function timeline(d, width) {
|
|
743
|
+
const evs = d.keyEvents.map((k) => ({ k, min: parseInt(k.clock, 10) })).filter((e) => Number.isFinite(e.min) && e.min >= 0);
|
|
744
|
+
if (evs.length === 0) return "";
|
|
745
|
+
const duration = evs.some((e) => e.min > 90) ? 120 : 90;
|
|
746
|
+
const T = Math.max(20, Math.min(width - 14, 64));
|
|
747
|
+
const cells = Array.from({ length: T }, () => ({
|
|
748
|
+
txt: pc3.gray("\u2500"),
|
|
749
|
+
w: 1,
|
|
750
|
+
used: false
|
|
751
|
+
}));
|
|
752
|
+
const htPos = Math.round(45 / duration * (T - 1));
|
|
753
|
+
cells[htPos] = { txt: pc3.white("\u2503"), w: 1, used: false };
|
|
754
|
+
const markFor = (k) => {
|
|
755
|
+
switch (k.kind) {
|
|
756
|
+
case "goal":
|
|
757
|
+
case "penalty-goal":
|
|
758
|
+
case "own-goal":
|
|
759
|
+
return { txt: SYM.goal, w: 2 };
|
|
760
|
+
case "yellow":
|
|
761
|
+
return { txt: pc3.yellow("\u25AE"), w: 1 };
|
|
762
|
+
case "red":
|
|
763
|
+
case "yellow-red":
|
|
764
|
+
return { txt: pc3.bold(pc3.red("\u25AE")), w: 1 };
|
|
765
|
+
case "penalty-miss":
|
|
766
|
+
return { txt: pc3.red("\xD7"), w: 1 };
|
|
767
|
+
default:
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
for (const { k, min } of evs.sort((a, b) => a.min - b.min)) {
|
|
772
|
+
const mark = markFor(k);
|
|
773
|
+
if (!mark) continue;
|
|
774
|
+
let pos = Math.round(Math.min(min, duration) / duration * (T - 1));
|
|
775
|
+
while (pos < T - 1 && cells[pos].used) pos += 1;
|
|
776
|
+
if (cells[pos].used) continue;
|
|
777
|
+
cells[pos] = { ...mark, used: true };
|
|
778
|
+
if (mark.w === 2 && pos + 1 < T && !cells[pos + 1].used) cells[pos + 1] = { txt: "", w: 0, used: true };
|
|
779
|
+
}
|
|
780
|
+
const endLabel = d.state === "post" ? "FT" : `${duration}'`;
|
|
781
|
+
return ` ${pc3.gray("0'")} ${cells.map((cl) => cl.txt).join("")} ${pc3.gray(endLabel)}`;
|
|
782
|
+
}
|
|
783
|
+
function statBars(d, width, maxRows) {
|
|
784
|
+
if (!d || d.stats.length === 0 || maxRows <= 0) return [];
|
|
785
|
+
const LABELW = 11;
|
|
786
|
+
const lines = [];
|
|
787
|
+
const B2 = Math.max(6, Math.min(Math.floor((width - LABELW - 16) / 2), 16));
|
|
788
|
+
const poss = d.stats.find((st) => st.label === "Possession");
|
|
789
|
+
if (poss) {
|
|
790
|
+
const h = parseFloat(poss.home);
|
|
791
|
+
const a = parseFloat(poss.away);
|
|
792
|
+
if (Number.isFinite(h) && Number.isFinite(a)) {
|
|
793
|
+
lines.push(
|
|
794
|
+
` ${pc3.gray(padEndW("Possession", LABELW))} ${pc3.bold(String(poss.home).padStart(4))} ${splitBar(h, a, B2 * 2 + 1)} ${pc3.bold(poss.away)}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (const label of ["Shots", "On target", "Corners", "Saves", "Fouls"]) {
|
|
799
|
+
if (lines.length >= maxRows) break;
|
|
800
|
+
const st = d.stats.find((x) => x.label === label);
|
|
801
|
+
if (!st) continue;
|
|
802
|
+
const h = parseFloat(st.home);
|
|
803
|
+
const a = parseFloat(st.away);
|
|
804
|
+
if (!Number.isFinite(h) || !Number.isFinite(a)) continue;
|
|
805
|
+
const max = Math.max(h, a, 1);
|
|
806
|
+
lines.push(
|
|
807
|
+
` ${pc3.gray(padEndW(label, LABELW))} ${pc3.bold(String(st.home).padStart(4))} ${miniBarRev(h, max, B2, HOME_C)}${pc3.gray("\u258F")}${miniBar(a, max, B2, AWAY_C)} ${pc3.bold(st.away)}`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
return lines.slice(0, maxRows);
|
|
555
811
|
}
|
|
556
812
|
function commentaryTable(d, width, budget) {
|
|
557
|
-
const textCol = Math.max(
|
|
813
|
+
const textCol = Math.max(28, width - 13);
|
|
558
814
|
const wrapW = textCol - 2;
|
|
559
815
|
const t = makeTable({
|
|
560
|
-
head: [pc3.gray("Min"), pc3.gray("Commentary")],
|
|
561
|
-
colWidths: [6, textCol],
|
|
816
|
+
head: ["", pc3.gray("Min"), pc3.gray("Commentary")],
|
|
817
|
+
colWidths: [3, 6, textCol],
|
|
562
818
|
wordWrap: true
|
|
563
819
|
});
|
|
564
820
|
let used = 0;
|
|
565
821
|
for (const line of d.commentary) {
|
|
566
822
|
const text = truncate(line.text, wrapW * 2);
|
|
567
|
-
const wrapped = Math.max(1, Math.ceil(
|
|
823
|
+
const wrapped = Math.max(1, Math.ceil(stringWidth3(text) / wrapW));
|
|
568
824
|
if (used + wrapped > budget && used > 0) break;
|
|
569
825
|
used += wrapped;
|
|
570
|
-
|
|
826
|
+
const colorize = commentaryColor(line.text, line.isGoal);
|
|
827
|
+
t.push([commentaryMark(line.text, line.isGoal), colorize(line.time), colorize(text)]);
|
|
571
828
|
}
|
|
572
829
|
if (used === 0) return pc3.gray(" Awaiting first updates\u2026");
|
|
573
830
|
return t.toString();
|
|
@@ -582,35 +839,38 @@ function preNote(m, width) {
|
|
|
582
839
|
}
|
|
583
840
|
function footer(s) {
|
|
584
841
|
const pitch = s.showPitch ? "[p] pitch\u2713" : "[p] pitch";
|
|
585
|
-
return pc3.gray(` [\u2191\u2193] navigate \xB7 ${pitch} \xB7 [q] quit \xB7 data: ESPN (unofficial) \xB7 ${s.league}`);
|
|
842
|
+
return pc3.gray(` [\u2191\u2193/jk] navigate \xB7 ${pitch} \xB7 [r] refresh \xB7 [q] quit \xB7 data: ESPN (unofficial) \xB7 ${s.league}`);
|
|
586
843
|
}
|
|
587
844
|
function renderFrame(s) {
|
|
588
845
|
const W = Math.min((s.cols || 80) - 2, 118);
|
|
589
846
|
if (s.error && s.matches.length === 0) {
|
|
590
|
-
return
|
|
591
|
-
${pc3.
|
|
847
|
+
return [
|
|
848
|
+
`${badge("OFFLINE", 196, pc3.red)} ${pc3.red(`Could not reach ESPN: ${s.error}`)}`,
|
|
849
|
+
`${c.accent(spinner(s.tick))} ${pc3.gray("Will keep retrying\u2026")}`
|
|
850
|
+
].join("\n");
|
|
592
851
|
}
|
|
593
852
|
if (s.matches.length === 0) {
|
|
594
853
|
return `${headerLine(s, W)}
|
|
854
|
+
${headerRule(W)}
|
|
595
855
|
${pc3.gray(" No matches found for this view.")}`;
|
|
596
856
|
}
|
|
597
857
|
const cur = s.matches[s.selected] ?? null;
|
|
598
|
-
const out = [headerLine(s, W),
|
|
858
|
+
const out = [headerLine(s, W), headerRule(W), fixturesList(s, W)];
|
|
599
859
|
if (cur) {
|
|
600
|
-
out.push("",
|
|
860
|
+
out.push("", ...scoreboard(s, cur));
|
|
601
861
|
const d = s.detail && s.detail.id === cur.id ? s.detail : null;
|
|
602
862
|
if (cur.state === "pre") {
|
|
603
863
|
out.push(preNote(cur, W));
|
|
604
864
|
} else if (s.detailLoading && !d) {
|
|
605
|
-
out.push(pc3.gray("
|
|
865
|
+
out.push(` ${c.accent(spinner(s.tick))} ${pc3.gray("loading match\u2026")}`);
|
|
606
866
|
} else if (d) {
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
867
|
+
const tl = timeline(d, W);
|
|
868
|
+
if (tl) out.push(tl);
|
|
869
|
+
out.push(...statBars(d, W, 4));
|
|
609
870
|
const hasPos = d.commentary.some((cl) => cl.x != null && cl.y != null);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
const budget = Math.min(40, Math.max(4, (s.rows || 24) - overhead));
|
|
871
|
+
if (s.showPitch && hasPos) out.push(renderPitch(d, W));
|
|
872
|
+
const onScreen = out.reduce((n, blk) => n + 1 + (blk.match(/\n/g)?.length ?? 0), 0);
|
|
873
|
+
const budget = Math.min(40, Math.max(4, (s.rows || 24) - onScreen - 6));
|
|
614
874
|
out.push(commentaryTable(d, W, budget));
|
|
615
875
|
}
|
|
616
876
|
}
|
|
@@ -619,6 +879,7 @@ ${pc3.gray(" No matches found for this view.")}`;
|
|
|
619
879
|
}
|
|
620
880
|
|
|
621
881
|
// src/app.ts
|
|
882
|
+
var FLASH_TICKS = 6;
|
|
622
883
|
async function runApp(opts) {
|
|
623
884
|
const boardMs = Math.max(5, opts.refreshSec) * 1e3;
|
|
624
885
|
const detailMs = Math.max(5, Math.min(opts.refreshSec, 12)) * 1e3;
|
|
@@ -630,7 +891,10 @@ async function runApp(opts) {
|
|
|
630
891
|
updatedAt: null,
|
|
631
892
|
detailLoading: false,
|
|
632
893
|
countdown: null,
|
|
633
|
-
showPitch: true
|
|
894
|
+
showPitch: true,
|
|
895
|
+
tick: 0,
|
|
896
|
+
lastScores: /* @__PURE__ */ new Map(),
|
|
897
|
+
flashUntil: /* @__PURE__ */ new Map()
|
|
634
898
|
};
|
|
635
899
|
const draw = () => logUpdate(
|
|
636
900
|
renderFrame({
|
|
@@ -645,13 +909,23 @@ async function runApp(opts) {
|
|
|
645
909
|
detailLoading: state.detailLoading,
|
|
646
910
|
countdown: state.countdown,
|
|
647
911
|
showPitch: state.showPitch,
|
|
648
|
-
|
|
649
|
-
|
|
912
|
+
tick: state.tick,
|
|
913
|
+
flashIds: [...state.flashUntil.entries()].filter(([, u]) => u > state.tick).map(([id]) => id),
|
|
914
|
+
cols: process2.stdout.columns ?? (Number(process2.env.COLUMNS) || 80),
|
|
915
|
+
rows: process2.stdout.rows ?? (Number(process2.env.LINES) || 24)
|
|
650
916
|
})
|
|
651
917
|
);
|
|
652
918
|
const loadBoard = async () => {
|
|
653
919
|
try {
|
|
654
920
|
const matches = await fetchScoreboard(opts.league, opts.date);
|
|
921
|
+
for (const m of matches) {
|
|
922
|
+
const key = `${m.home.score ?? ""}-${m.away.score ?? ""}`;
|
|
923
|
+
const prev = state.lastScores.get(m.id);
|
|
924
|
+
if (prev !== void 0 && prev !== key && m.state === "in") {
|
|
925
|
+
state.flashUntil.set(m.id, state.tick + FLASH_TICKS);
|
|
926
|
+
}
|
|
927
|
+
state.lastScores.set(m.id, key);
|
|
928
|
+
}
|
|
655
929
|
state.matches = matches;
|
|
656
930
|
state.error = null;
|
|
657
931
|
state.updatedAt = Date.now();
|
|
@@ -683,12 +957,13 @@ async function runApp(opts) {
|
|
|
683
957
|
const liveIdx = state.matches.findIndex((m) => m.state === "in");
|
|
684
958
|
if (liveIdx >= 0) state.selected = liveIdx;
|
|
685
959
|
await loadDetail();
|
|
686
|
-
|
|
687
|
-
|
|
960
|
+
if (opts.once || !process2.stdin.isTTY) {
|
|
961
|
+
draw();
|
|
688
962
|
logUpdate.done();
|
|
689
963
|
return;
|
|
690
964
|
}
|
|
691
965
|
state.countdown = Math.round(boardMs / 1e3);
|
|
966
|
+
draw();
|
|
692
967
|
const boardTimer = setInterval(async () => {
|
|
693
968
|
await loadBoard();
|
|
694
969
|
state.countdown = Math.round(boardMs / 1e3);
|
|
@@ -699,21 +974,28 @@ async function runApp(opts) {
|
|
|
699
974
|
draw();
|
|
700
975
|
}, detailMs);
|
|
701
976
|
const tick = setInterval(() => {
|
|
977
|
+
state.tick += 1;
|
|
702
978
|
if (state.countdown != null) state.countdown = Math.max(0, state.countdown - 1);
|
|
979
|
+
for (const [id, until] of state.flashUntil) {
|
|
980
|
+
if (until <= state.tick) state.flashUntil.delete(id);
|
|
981
|
+
}
|
|
703
982
|
draw();
|
|
704
983
|
}, 1e3);
|
|
984
|
+
const onResize = () => draw();
|
|
985
|
+
process2.stdout.on("resize", onResize);
|
|
705
986
|
const cleanup = () => {
|
|
706
987
|
clearInterval(boardTimer);
|
|
707
988
|
clearInterval(detailTimer);
|
|
708
989
|
clearInterval(tick);
|
|
709
|
-
|
|
990
|
+
process2.stdout.off("resize", onResize);
|
|
991
|
+
process2.stdin.off("keypress", onKey);
|
|
710
992
|
try {
|
|
711
|
-
|
|
993
|
+
process2.stdin.setRawMode(false);
|
|
712
994
|
} catch {
|
|
713
995
|
}
|
|
714
996
|
logUpdate.done();
|
|
715
|
-
|
|
716
|
-
|
|
997
|
+
process2.stdout.write("\n");
|
|
998
|
+
process2.exit(0);
|
|
717
999
|
};
|
|
718
1000
|
const reselect = async (delta) => {
|
|
719
1001
|
const next = Math.min(state.matches.length - 1, Math.max(0, state.selected + delta));
|
|
@@ -735,23 +1017,34 @@ async function runApp(opts) {
|
|
|
735
1017
|
else if (key.name === "p") {
|
|
736
1018
|
state.showPitch = !state.showPitch;
|
|
737
1019
|
draw();
|
|
738
|
-
} else if (key.name === "r")
|
|
1020
|
+
} else if (key.name === "r") {
|
|
1021
|
+
void loadBoard().then(() => {
|
|
1022
|
+
state.countdown = Math.round(boardMs / 1e3);
|
|
1023
|
+
draw();
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
739
1026
|
};
|
|
740
|
-
readline.emitKeypressEvents(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1027
|
+
readline.emitKeypressEvents(process2.stdin);
|
|
1028
|
+
process2.stdin.setRawMode(true);
|
|
1029
|
+
process2.stdin.resume();
|
|
1030
|
+
process2.stdin.on("keypress", onKey);
|
|
744
1031
|
}
|
|
745
1032
|
|
|
746
1033
|
// bin/index.ts
|
|
747
|
-
var argv =
|
|
1034
|
+
var argv = process3.argv.slice(2);
|
|
748
1035
|
function opt(name) {
|
|
749
1036
|
const i = argv.indexOf(name);
|
|
750
|
-
|
|
1037
|
+
if (i < 0) return void 0;
|
|
1038
|
+
const v = argv[i + 1];
|
|
1039
|
+
return v !== void 0 && !v.startsWith("--") ? v : void 0;
|
|
751
1040
|
}
|
|
752
1041
|
function flag(name) {
|
|
753
1042
|
return argv.includes(name);
|
|
754
1043
|
}
|
|
1044
|
+
function fail(msg) {
|
|
1045
|
+
console.error(`soccer: ${msg}`);
|
|
1046
|
+
process3.exit(1);
|
|
1047
|
+
}
|
|
755
1048
|
if (flag("--help") || flag("-h")) {
|
|
756
1049
|
console.log(`
|
|
757
1050
|
\u26BD soccer \u2014 live football scores & commentary in your terminal
|
|
@@ -770,6 +1063,7 @@ if (flag("--help") || flag("-h")) {
|
|
|
770
1063
|
|
|
771
1064
|
Keys
|
|
772
1065
|
\u2191/\u2193 or j/k Move between matches
|
|
1066
|
+
p Toggle pitch view
|
|
773
1067
|
r Force refresh
|
|
774
1068
|
q or Esc Quit
|
|
775
1069
|
|
|
@@ -780,12 +1074,30 @@ if (flag("--help") || flag("-h")) {
|
|
|
780
1074
|
|
|
781
1075
|
Data: ESPN's public (unofficial) feed. No API key required.
|
|
782
1076
|
`);
|
|
783
|
-
|
|
1077
|
+
process3.exit(0);
|
|
784
1078
|
}
|
|
785
1079
|
var league = opt("--league") ?? "fifa.world";
|
|
786
1080
|
var date = opt("--date");
|
|
787
|
-
|
|
788
|
-
|
|
1081
|
+
if (flag("--date")) {
|
|
1082
|
+
if (!date || !/^\d{8}$/.test(date)) {
|
|
1083
|
+
fail(`invalid --date "${date ?? ""}" \u2014 expected YYYYMMDD, e.g. 20260611`);
|
|
1084
|
+
}
|
|
1085
|
+
const mm = Number(date.slice(4, 6));
|
|
1086
|
+
const dd = Number(date.slice(6, 8));
|
|
1087
|
+
if (mm < 1 || mm > 12 || dd < 1 || dd > 31) {
|
|
1088
|
+
fail(`invalid --date "${date}" \u2014 month must be 01-12 and day 01-31`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
var refreshSec = 15;
|
|
1092
|
+
if (flag("--refresh")) {
|
|
1093
|
+
const raw = opt("--refresh");
|
|
1094
|
+
const n = Number(raw);
|
|
1095
|
+
if (raw === void 0 || !Number.isFinite(n) || n <= 0) {
|
|
1096
|
+
fail(`invalid --refresh "${raw ?? ""}" \u2014 expected a positive number of seconds`);
|
|
1097
|
+
}
|
|
1098
|
+
refreshSec = Math.max(5, n);
|
|
1099
|
+
}
|
|
1100
|
+
var once = flag("--once") || !process3.stdin.isTTY;
|
|
789
1101
|
var TITLES = {
|
|
790
1102
|
"fifa.world": "FIFA WORLD CUP 2026",
|
|
791
1103
|
"eng.1": "PREMIER LEAGUE",
|
|
@@ -798,5 +1110,5 @@ var TITLES = {
|
|
|
798
1110
|
var title = opt("--title") ?? TITLES[league] ?? league.toUpperCase();
|
|
799
1111
|
runApp({ league, title, date, refreshSec, once }).catch((err) => {
|
|
800
1112
|
console.error(err);
|
|
801
|
-
|
|
1113
|
+
process3.exit(1);
|
|
802
1114
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soccer-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Live football scores, ball-by-ball commentary, and a live ASCII pitch in your terminal. Built for the 2026 World Cup, works for every major league. No API key required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|