@claudinho/cli 0.4.3 → 0.5.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 (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +198 -39
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -21,11 +21,11 @@ npx @claudinho/cli today
21
21
  claudinho today [date] # a day's fixtures in your timezone (default: today), live scores inline
22
22
  claudinho live # matches in play right now
23
23
  claudinho next [TEAM] # a team's next fixture + countdown (default: $CLAUDINHO_TEAM)
24
- claudinho table [GROUP] # group standings (default: all groups)
24
+ claudinho table [GROUP] # live cumulative group standings (default: all groups)
25
25
  claudinho match <id> # a single match's detail
26
26
  claudinho markets [target] # prediction-market signals: today | <date> | <id> | next <TEAM>
27
27
  # (next prefers the team's IN-PLAY match while one is live)
28
- claudinho share [target] # copy-pasteable match snippet: today | live | <date> | <id> | next <TEAM>
28
+ claudinho share [target] # copy-pasteable snippet: today | live | <date> | <id> | next <TEAM> | table <GROUP>
29
29
  claudinho prompt # one compact status line (for statusline/tmux/Starship)
30
30
  claudinho init-statusline # wire it into the Claude Code statusline
31
31
  claudinho hook # live-score context for a Claude Code hook (silent off-match)
@@ -108,6 +108,7 @@ social posts, READMEs, and issue comments — your terminal football, ready to p
108
108
  claudinho share # today's matches
109
109
  claudinho share live # matches in play
110
110
  claudinho share next MEX # a team's next fixture (+ market read, when reliable)
111
+ claudinho share table A # a group's standings card (facts only, no market line)
111
112
  claudinho share 760415 # one match by id
112
113
  claudinho share next MEX --copy # …and copy it straight to the clipboard
113
114
  ```
package/dist/index.js CHANGED
@@ -2762,6 +2762,43 @@ function mapEspnEvent(ev, ctx = {}) {
2762
2762
  function toEspnDate(d) {
2763
2763
  return d.replace(/\D/g, "").slice(0, 8);
2764
2764
  }
2765
+ function statVal(stats, name) {
2766
+ const v = stats?.find((s) => s.name === name)?.value;
2767
+ return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
2768
+ }
2769
+ function entryToRow(e) {
2770
+ return {
2771
+ team: toTeam(e.team),
2772
+ played: statVal(e.stats, "gamesPlayed"),
2773
+ won: statVal(e.stats, "wins"),
2774
+ drawn: statVal(e.stats, "ties"),
2775
+ lost: statVal(e.stats, "losses"),
2776
+ goalsFor: statVal(e.stats, "pointsFor"),
2777
+ goalsAgainst: statVal(e.stats, "pointsAgainst"),
2778
+ goalDiff: statVal(e.stats, "pointDifferential"),
2779
+ points: statVal(e.stats, "points")
2780
+ };
2781
+ }
2782
+ function parseStandings(data) {
2783
+ const out2 = [];
2784
+ for (const child of data.children ?? []) {
2785
+ const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2786
+ if (!letter) continue;
2787
+ const ranked = (child.standings?.entries ?? []).map((e) => ({
2788
+ row: entryToRow(e),
2789
+ rank: statVal(e.stats, "rank")
2790
+ }));
2791
+ ranked.sort((a, b) => {
2792
+ if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
2793
+ const r = a.row;
2794
+ const s = b.row;
2795
+ return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
2796
+ });
2797
+ out2.push({ group: letter, rows: ranked.map((x) => x.row) });
2798
+ }
2799
+ out2.sort((a, b) => a.group.localeCompare(b.group));
2800
+ return out2;
2801
+ }
2765
2802
  var EspnAdapter = class {
2766
2803
  constructor(opts = {}) {
2767
2804
  this.opts = opts;
@@ -2781,25 +2818,30 @@ var EspnAdapter = class {
2781
2818
  const today = await this.fetchScoreboard();
2782
2819
  return today.filter((m) => m.status === "LIVE" || m.status === "HT");
2783
2820
  }
2821
+ /** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
2822
+ standingsUrl() {
2823
+ const base = this.opts.baseUrl ?? DEFAULT_BASE;
2824
+ return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2825
+ }
2826
+ /**
2827
+ * Authoritative, cumulative group tables from the standings endpoint. Throws
2828
+ * on fetch/parse failure (the caller decides the fallback). Group-stage only:
2829
+ * non-group `children` are filtered out by {@link parseStandings}.
2830
+ */
2831
+ async fetchStandings() {
2832
+ return parseStandings(await this.get(this.standingsUrl()));
2833
+ }
2784
2834
  /**
2785
2835
  * Build (and cache) a team-code -> group-letter map from the standings
2786
- * endpoint. Best-effort: returns {} if standings are unavailable.
2836
+ * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
2837
+ * same parse as {@link fetchStandings}, so the two never drift.
2787
2838
  */
2788
2839
  async fetchGroupMap(force = false) {
2789
2840
  if (this.groupMap && !force) return this.groupMap;
2790
- const base = this.opts.baseUrl ?? DEFAULT_BASE;
2791
- const standingsUrl = `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2792
2841
  const map = {};
2793
2842
  try {
2794
- const data = await this.get(standingsUrl);
2795
- for (const child of data.children ?? []) {
2796
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2797
- if (!letter) continue;
2798
- for (const e of child.standings?.entries ?? []) {
2799
- const code = e.team?.abbreviation?.toUpperCase();
2800
- if (code) map[code] = letter;
2801
- }
2802
- }
2843
+ const tables = parseStandings(await this.get(this.standingsUrl()));
2844
+ for (const t of tables) for (const r of t.rows) map[r.team.code] = t.group;
2803
2845
  } catch {
2804
2846
  }
2805
2847
  this.groupMap = map;
@@ -2870,6 +2912,22 @@ async function getMatchesForDate(adapter, dateISO) {
2870
2912
  return { matches: base, degraded: true };
2871
2913
  }
2872
2914
  }
2915
+ async function getStandings(adapter, group) {
2916
+ const want = group?.toUpperCase();
2917
+ if (adapter.fetchStandings) {
2918
+ try {
2919
+ const all = await adapter.fetchStandings();
2920
+ const tables2 = (want ? all.filter((t) => t.group === want) : all).sort(
2921
+ (a, b) => a.group.localeCompare(b.group)
2922
+ );
2923
+ return { tables: tables2, degraded: false, source: adapter.name };
2924
+ } catch {
2925
+ }
2926
+ }
2927
+ const letters = want ? [want] : groups();
2928
+ const tables = letters.map((g) => ({ group: g, rows: computeStandings(fixturesByGroup(g)) })).filter((t) => t.rows.length > 0);
2929
+ return { tables, degraded: true };
2930
+ }
2873
2931
  function shiftUtcDate(dateISO, days) {
2874
2932
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
2875
2933
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
@@ -3402,11 +3460,50 @@ function formatShareSnippet(input, options = {}) {
3402
3460
  blocks.push(card.join("\n"));
3403
3461
  }
3404
3462
  }
3463
+ blocks.push(
3464
+ shareFooter({
3465
+ source: input.source,
3466
+ installLine: input.installLine,
3467
+ includeHashtag,
3468
+ includeInstall
3469
+ })
3470
+ );
3471
+ return blocks.join("\n\n");
3472
+ }
3473
+ function shareFooter(opts) {
3405
3474
  const footer = [];
3406
- if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3407
- footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3408
- if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3409
- blocks.push(footer.join("\n"));
3475
+ if (opts.source) footer.push(`Live data: ${liveSourceLabel(opts.source)}`);
3476
+ footer.push(
3477
+ [opts.includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 ")
3478
+ );
3479
+ if (opts.includeInstall && opts.installLine) footer.push(`Try it: ${opts.installLine}`);
3480
+ return footer.join("\n");
3481
+ }
3482
+ function gd(n) {
3483
+ return n > 0 ? `+${n}` : `${n}`;
3484
+ }
3485
+ function tableRow(r, rank) {
3486
+ return `${rank}. ${r.team.flag} ${r.team.code} ${r.points} pts \xB7 ${r.won}-${r.drawn}-${r.lost} \xB7 ${gd(r.goalDiff)}`;
3487
+ }
3488
+ function formatShareTable(input, options = {}) {
3489
+ const includeHashtag = options.includeHashtag !== false;
3490
+ const includeInstall = options.includeInstallLine !== false;
3491
+ const blocks = [];
3492
+ if (input.tables.length === 0) {
3493
+ blocks.push(input.emptyNote ?? "No standings available.");
3494
+ } else {
3495
+ for (const { group, rows } of input.tables) {
3496
+ blocks.push(
3497
+ [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
3498
+ );
3499
+ }
3500
+ if (input.degraded) {
3501
+ blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
3502
+ }
3503
+ }
3504
+ blocks.push(
3505
+ shareFooter({ source: input.source, installLine: input.installLine, includeHashtag, includeInstall })
3506
+ );
3410
3507
  return blocks.join("\n\n");
3411
3508
  }
3412
3509
 
@@ -3465,6 +3562,8 @@ var EN = {
3465
3562
  "next.in": "in {countdown}",
3466
3563
  "table.title": "Group {group}",
3467
3564
  "table.none": "No group found for {group}.",
3565
+ "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
3566
+ "table.empty": "No standings available.",
3468
3567
  "match.none": "No match found with id {id}.",
3469
3568
  "status.scheduled": "scheduled",
3470
3569
  "status.live": "LIVE",
@@ -3495,6 +3594,8 @@ var ES = {
3495
3594
  "next.in": "en {countdown}",
3496
3595
  "table.title": "Grupo {group}",
3497
3596
  "table.none": "No se encontr\xF3 el grupo {group}.",
3597
+ "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
3598
+ "table.empty": "No hay clasificaci\xF3n disponible.",
3498
3599
  "match.none": "No se encontr\xF3 partido con id {id}.",
3499
3600
  "status.scheduled": "programado",
3500
3601
  "status.live": "EN VIVO",
@@ -3525,6 +3626,8 @@ var PT = {
3525
3626
  "next.in": "em {countdown}",
3526
3627
  "table.title": "Grupo {group}",
3527
3628
  "table.none": "Grupo {group} n\xE3o encontrado.",
3629
+ "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
3630
+ "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
3528
3631
  "match.none": "Nenhum jogo encontrado com id {id}.",
3529
3632
  "status.scheduled": "agendado",
3530
3633
  "status.live": "AO VIVO",
@@ -3555,6 +3658,8 @@ var FR = {
3555
3658
  "next.in": "dans {countdown}",
3556
3659
  "table.title": "Groupe {group}",
3557
3660
  "table.none": "Groupe {group} introuvable.",
3661
+ "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
3662
+ "table.empty": "Aucun classement disponible.",
3558
3663
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
3559
3664
  "status.scheduled": "pr\xE9vu",
3560
3665
  "status.live": "DIRECT",
@@ -4218,10 +4323,10 @@ async function cmdLive(ctx) {
4218
4323
  if (src) out(src);
4219
4324
  out(disclaimer(t, c));
4220
4325
  }
4221
- async function cmdNext(team, { cfg, t }) {
4326
+ async function cmdNext(team, { cfg, t, now }) {
4222
4327
  precheck(cfg, t);
4223
4328
  const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)");
4224
- const fixture = nextFixtureForTeam(code);
4329
+ const fixture = nextFixtureForTeam(code, { from: now ?? /* @__PURE__ */ new Date() });
4225
4330
  if (cfg.json) {
4226
4331
  emitJson({ team: code, fixture: fixture ?? null });
4227
4332
  return;
@@ -4248,32 +4353,30 @@ async function cmdNext(team, { cfg, t }) {
4248
4353
  async function cmdTable(group, ctx) {
4249
4354
  const { cfg, t } = ctx;
4250
4355
  precheck(cfg, t);
4251
- const adapter = adapterFor(ctx);
4252
- const { matches, degraded, source } = await getMatchesForDate(
4253
- adapter,
4254
- localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz)
4255
- );
4256
- const wanted = group ? [group.toUpperCase()] : groups(matches);
4356
+ const { tables, degraded, source } = await getStandings(adapterFor(ctx), group);
4257
4357
  if (cfg.json) {
4258
- const tables = wanted.map((g) => ({
4259
- group: g,
4260
- standings: computeStandings(fixturesByGroup(g, matches))
4261
- }));
4358
+ const json = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
4262
4359
  emitJson({
4263
4360
  degraded,
4264
4361
  source: source ?? null,
4265
- tables: group ? tables[0] ?? null : tables
4362
+ tables: group ? json[0] ?? null : json
4266
4363
  });
4267
4364
  return;
4268
4365
  }
4269
4366
  const c = painterFor(cfg);
4270
- for (const g of wanted) {
4271
- const rows = computeStandings(fixturesByGroup(g, matches));
4272
- if (rows.length === 0) {
4273
- out();
4274
- out(c.dim(" " + t("table.none", { group: g })));
4275
- continue;
4276
- }
4367
+ if (tables.length === 0) {
4368
+ out();
4369
+ out(
4370
+ c.dim(
4371
+ " " + (group ? t("table.none", { group: group.toUpperCase() }) : t("table.empty"))
4372
+ )
4373
+ );
4374
+ out();
4375
+ if (degraded) out(c.dim(" " + t("table.degraded")));
4376
+ out(disclaimer(t, c));
4377
+ return;
4378
+ }
4379
+ for (const { group: g, rows } of tables) {
4277
4380
  out();
4278
4381
  out(header(t("table.title", { group: g }), c));
4279
4382
  const table = new Table({
@@ -4303,6 +4406,7 @@ async function cmdTable(group, ctx) {
4303
4406
  out(table.toString());
4304
4407
  }
4305
4408
  out();
4409
+ if (degraded) out(c.dim(" " + t("table.degraded")));
4306
4410
  const src = dataSource(source, c);
4307
4411
  if (src) out(src);
4308
4412
  out(disclaimer(t, c));
@@ -4541,6 +4645,38 @@ function emitShare(ctx, e, copy) {
4541
4645
  );
4542
4646
  }
4543
4647
  }
4648
+ function emitShareTable(ctx, e, copy) {
4649
+ const snippet = formatShareTable(
4650
+ {
4651
+ tables: e.tables,
4652
+ source: e.source,
4653
+ installLine: e.installLine,
4654
+ emptyNote: e.emptyNote,
4655
+ degraded: e.degraded
4656
+ },
4657
+ e.options
4658
+ );
4659
+ if (ctx.cfg.json) {
4660
+ emitJson({
4661
+ kind: "table",
4662
+ target: "table",
4663
+ ...e.group ? { group: e.group } : {},
4664
+ source: e.source ?? null,
4665
+ degraded: e.degraded,
4666
+ informationalOnly: true,
4667
+ snippet,
4668
+ tables: e.tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
4669
+ });
4670
+ } else {
4671
+ out(snippet);
4672
+ }
4673
+ if (copy) {
4674
+ const ok = (ctx.copy ?? copyToClipboard)(snippet);
4675
+ process.stderr.write(
4676
+ (ok ? "Copied share snippet to clipboard." : "Clipboard unavailable; printed snippet instead.") + "\n"
4677
+ );
4678
+ }
4679
+ }
4544
4680
  async function cmdShare(target, team, opts, ctx) {
4545
4681
  const { cfg, t } = ctx;
4546
4682
  const baseOptions = {
@@ -4574,10 +4710,30 @@ async function cmdShare(target, team, opts, ctx) {
4574
4710
  );
4575
4711
  return;
4576
4712
  }
4713
+ if (target === "table") {
4714
+ precheck(cfg, t);
4715
+ const group = team?.toUpperCase();
4716
+ const { tables, degraded, source: source2 } = await getStandings(adapterFor(ctx), group);
4717
+ emitShareTable(
4718
+ ctx,
4719
+ {
4720
+ group,
4721
+ tables,
4722
+ // Degraded ⇒ a static roster, served by no live provider: no attribution.
4723
+ source: degraded ? void 0 : source2,
4724
+ degraded,
4725
+ installLine: group ? `npx @claudinho/cli table ${group}` : "npx @claudinho/cli table",
4726
+ emptyNote: group ? `No group ${group}.` : "No standings available.",
4727
+ options: baseOptions
4728
+ },
4729
+ copy
4730
+ );
4731
+ return;
4732
+ }
4577
4733
  if (target === "next") {
4578
4734
  precheck(cfg, t);
4579
4735
  const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)");
4580
- const fixture = nextFixtureForTeam(code);
4736
+ const fixture = nextFixtureForTeam(code, { from: ctx.now ?? /* @__PURE__ */ new Date() });
4581
4737
  const matches = fixture ? [fixture] : [];
4582
4738
  const signals2 = await reliableShareSignals(ctx, matches);
4583
4739
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
@@ -4726,7 +4882,7 @@ function handlePipeError(stream) {
4726
4882
  }
4727
4883
  handlePipeError(process.stdout);
4728
4884
  handlePipeError(process.stderr);
4729
- var VERSION = "0.4.3";
4885
+ var VERSION = "0.5.0";
4730
4886
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
4731
4887
  function ctxFrom(cmd) {
4732
4888
  const root = cmd.parent ?? cmd;
@@ -4787,7 +4943,10 @@ program.command("markets").description("show prediction-market signals (read-onl
4787
4943
  fail(e);
4788
4944
  }
4789
4945
  });
4790
- program.command("share").description("print a shareable, copy-pasteable match snippet (#VibingLaVidaLoca)").argument("[target]", '"today" (default), "live", a date, a match id, or "next"').argument("[team]", 'team code when target is "next" (default: $CLAUDINHO_TEAM)').option("--style <style>", "snippet style: social (default) or compact").option("--copy", "also copy the snippet to the clipboard (best-effort)").option("--no-hashtag", "omit the #VibingLaVidaLoca tag").option("--no-install-line", "omit the install/run cue").action(async (target, team, opts, cmd) => {
4946
+ program.command("share").description("print a shareable, copy-pasteable match snippet (#VibingLaVidaLoca)").argument("[target]", '"today" (default), "live", a date, a match id, "next", or "table"').argument(
4947
+ "[team]",
4948
+ 'team code for "next" (default: $CLAUDINHO_TEAM), or group letter for "table" (omit for all groups)'
4949
+ ).option("--style <style>", "snippet style: social (default) or compact").option("--copy", "also copy the snippet to the clipboard (best-effort)").option("--no-hashtag", "omit the #VibingLaVidaLoca tag").option("--no-install-line", "omit the install/run cue").action(async (target, team, opts, cmd) => {
4791
4950
  try {
4792
4951
  await cmdShare(target, team, opts, ctxFrom(cmd));
4793
4952
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Live scores, fixtures, group tables, and read-only prediction-market signals for the 2026 men's football tournament — in your terminal and Claude Code statusline. No API keys. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,7 +56,7 @@
56
56
  "tsup": "^8.0.0",
57
57
  "typescript": "^5.7.0",
58
58
  "vitest": "^4.1.0",
59
- "@claudinho/core": "0.4.3"
59
+ "@claudinho/core": "0.5.0"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsup",