@claudinho/cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +67 -0
  2. package/dist/index.js +295 -3
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -24,6 +24,7 @@ claudinho next <TEAM> # a team's next fixture + countdown (e.g. next MEX
24
24
  claudinho table [GROUP] # group standings (default: all groups)
25
25
  claudinho match <id> # a single match's detail
26
26
  claudinho markets [target] # prediction-market odds: today | <date> | <id> | next <TEAM>
27
+ claudinho share [target] # copy-pasteable match snippet: today | live | <date> | <id> | next <TEAM>
27
28
  claudinho prompt # one compact status line (for statusline/tmux/Starship)
28
29
  claudinho init-statusline # wire it into the Claude Code statusline
29
30
  claudinho hook # live-score context for a Claude Code hook (silent off-match)
@@ -39,6 +40,7 @@ claudinho next BRA --tz America/Sao_Paulo --lang pt
39
40
  claudinho table A
40
41
  claudinho live --json | jq '.matches[].status'
41
42
  claudinho today --flavor off # just the facts, no commentary
43
+ claudinho share next MEX --copy # a shareable card, copied to your clipboard
42
44
  ```
43
45
 
44
46
  ## Global options
@@ -95,6 +97,71 @@ statusline and hook **never** show market data — it stays off the hot path.
95
97
  > nothing. For an offline preview, set `CLAUDINHO_MARKETS_SOURCE=fake` to render
96
98
  > clearly-labeled synthetic **"demo data"** odds.
97
99
 
100
+ ## Shareable snippets
101
+
102
+ `claudinho share` prints a polished, **copy-pasteable** match card for chats,
103
+ social posts, READMEs, and issue comments — your terminal football, ready to post:
104
+
105
+ ```bash
106
+ claudinho share # today's matches
107
+ claudinho share live # matches in play
108
+ claudinho share next MEX # a team's next fixture (+ market read, when reliable)
109
+ claudinho share 760415 # one match by id
110
+ claudinho share next MEX --copy # …and copy it straight to the clipboard
111
+ ```
112
+
113
+ ```text
114
+ Next up for Mexico
115
+
116
+ 🇲🇽 Mexico vs South Africa 🇿🇦
117
+ Jun 11 · 13:00 America/Mexico_City
118
+ Estadio Banorte, Mexico City, Mexico
119
+
120
+ Prediction markets slightly favor Mexico.
121
+ Mexico 56% · Draw 25% · South Africa 19%
122
+ Source: Polymarket · informational only
123
+
124
+ #VibingLaVidaLoca · Independent fan project · not affiliated with FIFA or Anthropic.
125
+ Try it: npx @claudinho/cli next MEX
126
+ ```
127
+
128
+ Snippets are **plain text** (no color codes — they paste cleanly everywhere) and
129
+ carry the non-affiliation disclaimer on every paste. The market line uses the
130
+ same reliable gate as `today`/`match` (**informational only — never betting
131
+ advice**) and disappears when no reliable market exists. Per-command options:
132
+
133
+ | Flag | Description |
134
+ |---|---|
135
+ | `--style <social\|compact>` | `social` (default) is the full card; `compact` is one terse line per match |
136
+ | `--copy` | also copy the snippet to the clipboard (best-effort: `pbcopy`/`clip`/`wl-copy`/`xclip`/`xsel`) |
137
+ | `--no-hashtag` | omit the `#VibingLaVidaLoca` tag |
138
+ | `--no-install-line` | omit the `Try it: …` run cue |
139
+
140
+ `--json` returns the structured snippet (`{ kind, snippet, matches, marketSignals, … }`)
141
+ for scripts and future reuse. No clipboard tool? `claudinho share … | pbcopy` works too.
142
+
143
+ ### Want an image?
144
+
145
+ The snippet is plain text, so a **screenshot is your share card** — the disclaimer
146
+ and emoji flags come along automatically. Grab it with your OS screenshot tool, or
147
+ render a crisp terminal image with an existing tool (no Claudinho dependency, no
148
+ account):
149
+
150
+ ```bash
151
+ # capture the command's output directly (charmbracelet/freeze)
152
+ freeze --execute "claudinho share next MEX" -o card.png
153
+
154
+ # …or pipe the text into freeze / silicon
155
+ claudinho share next MEX | freeze --language text -o card.png
156
+ claudinho share next MEX | silicon -l txt -o card.png
157
+
158
+ # …or paste it into a web tool like carbon.now.sh or ray.so
159
+ ```
160
+
161
+ Claudinho stays text-first — it doesn't ship an image renderer, so there's nothing
162
+ extra to install in the CLI and no fonts/licensing to worry about: whatever your
163
+ terminal or the screenshot tool already draws (including the flags) is what you get.
164
+
98
165
  ## Statusline (Claude Code)
99
166
 
100
167
  ```bash
package/dist/index.js CHANGED
@@ -311,6 +311,25 @@ function formatKickoff(iso, opts = {}) {
311
311
  timeZone: tz
312
312
  }).format(new Date(iso));
313
313
  }
314
+ function formatDate(iso, opts = {}) {
315
+ const tz = resolveTz(opts.tz);
316
+ const locale = safeLocale(opts.locale);
317
+ return new Intl.DateTimeFormat(locale, {
318
+ month: "short",
319
+ day: "numeric",
320
+ timeZone: tz
321
+ }).format(new Date(iso));
322
+ }
323
+ function formatTime(iso, opts = {}) {
324
+ const tz = resolveTz(opts.tz);
325
+ const locale = safeLocale(opts.locale);
326
+ return new Intl.DateTimeFormat(locale, {
327
+ hour: "2-digit",
328
+ minute: "2-digit",
329
+ hour12: false,
330
+ timeZone: tz
331
+ }).format(new Date(iso));
332
+ }
314
333
  function countdown(iso, from = /* @__PURE__ */ new Date()) {
315
334
  const ms = new Date(iso).getTime() - from.getTime();
316
335
  if (ms <= 0) return "now";
@@ -3269,6 +3288,87 @@ async function getMarketSignals(provider, matches, options) {
3269
3288
  return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
3270
3289
  }
3271
3290
  }
3291
+ var SHARE_HASHTAG = "#VibingLaVidaLoca";
3292
+ var SHARE_DISCLAIMER = "Independent fan project \xB7 not affiliated with FIFA or Anthropic.";
3293
+ function mid(m) {
3294
+ return isLive(m.status) || m.status === "FT" ? scoreline(m) : "vs";
3295
+ }
3296
+ function statusTail(m) {
3297
+ switch (m.status) {
3298
+ case "LIVE":
3299
+ return m.minute ? `${m.minute}'` : "LIVE";
3300
+ case "HT":
3301
+ return "HT";
3302
+ case "FT":
3303
+ return "FT";
3304
+ case "POSTPONED":
3305
+ return "postponed";
3306
+ case "CANCELLED":
3307
+ return "cancelled";
3308
+ default:
3309
+ return "";
3310
+ }
3311
+ }
3312
+ function compactLine(m, input, single) {
3313
+ const home = `${m.home.flag} ${m.home.code}`;
3314
+ const away = `${m.away.code} ${m.away.flag}`;
3315
+ const opts = { tz: input.tz, locale: input.locale };
3316
+ let tail;
3317
+ if (m.status === "SCHEDULED") {
3318
+ const time = formatTime(m.kickoff, opts);
3319
+ tail = single ? `${formatDate(m.kickoff, opts)} ${time}` : time;
3320
+ } else {
3321
+ tail = statusTail(m);
3322
+ }
3323
+ return `${home} ${mid(m)} ${away}${tail ? ` \xB7 ${tail}` : ""}`;
3324
+ }
3325
+ function socialCard(m, input) {
3326
+ const lines = [];
3327
+ const head = `${m.home.flag} ${m.home.name} ${mid(m)} ${m.away.name} ${m.away.flag}`;
3328
+ if (m.status === "SCHEDULED") {
3329
+ lines.push(head);
3330
+ const date = formatDate(m.kickoff, { tz: input.tz, locale: input.locale });
3331
+ const time = formatTime(m.kickoff, { tz: input.tz, locale: input.locale });
3332
+ const zone = input.tz ? ` ${input.tz}` : "";
3333
+ lines.push(`${date} \xB7 ${time}${zone}`);
3334
+ } else {
3335
+ const tail = statusTail(m);
3336
+ lines.push(tail ? `${head} \xB7 ${tail}` : head);
3337
+ }
3338
+ const loc = matchLocation(m);
3339
+ if (loc) lines.push(loc);
3340
+ return lines;
3341
+ }
3342
+ function formatShareSnippet(input, options = {}) {
3343
+ const style = options.style ?? "social";
3344
+ const includeMarkets = options.includeMarkets !== false;
3345
+ const includeHashtag = options.includeHashtag !== false;
3346
+ const includeInstall = options.includeInstallLine !== false;
3347
+ const signals = input.marketSignals ?? /* @__PURE__ */ new Map();
3348
+ const single = input.matches.length === 1;
3349
+ const blocks = [input.title];
3350
+ if (input.matches.length === 0) {
3351
+ if (input.emptyNote) blocks.push(input.emptyNote);
3352
+ } else if (style === "compact") {
3353
+ blocks.push(input.matches.map((m) => compactLine(m, input, single)).join("\n"));
3354
+ } else {
3355
+ for (const m of input.matches) {
3356
+ const card = socialCard(m, input);
3357
+ const sig = includeMarkets ? signals.get(m.id) : void 0;
3358
+ if (sig) {
3359
+ if (single) card.push("", ...marketBlock(sig, m));
3360
+ else card.push(marketLine(sig, m));
3361
+ }
3362
+ blocks.push(card.join("\n"));
3363
+ }
3364
+ }
3365
+ const footer = [];
3366
+ if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3367
+ footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3368
+ if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3369
+ blocks.push(footer.join("\n"));
3370
+ return blocks.join("\n\n");
3371
+ }
3272
3372
 
3273
3373
  // src/config.ts
3274
3374
  var SUPPORTED_LANGS = ["en", "es", "pt", "fr"];
@@ -3494,8 +3594,8 @@ function statusToken(m, t, c) {
3494
3594
  function matchLine(m, cfg, t, c) {
3495
3595
  const home = `${m.home.flag} ${m.home.name}`;
3496
3596
  const away = `${m.away.name} ${m.away.flag}`;
3497
- const mid = isLive(m.status) || m.status === "FT" ? c.bold(scoreline(m)) : c.dim("vs");
3498
- const left = `${home.padEnd(22)} ${mid.padStart(3)} ${away}`;
3597
+ const mid2 = isLive(m.status) || m.status === "FT" ? c.bold(scoreline(m)) : c.dim("vs");
3598
+ const left = `${home.padEnd(22)} ${mid2.padStart(3)} ${away}`;
3499
3599
  let right = "";
3500
3600
  if (m.status === "SCHEDULED") {
3501
3601
  right = c.dim(
@@ -3572,6 +3672,32 @@ function writeMarketCache(source, competition, attempted, fetched, now = Date.no
3572
3672
  }
3573
3673
  }
3574
3674
 
3675
+ // src/clipboard.ts
3676
+ import { spawnSync } from "child_process";
3677
+ function clipboardTools(platform) {
3678
+ if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
3679
+ if (platform === "win32") return [{ cmd: "clip", args: [] }];
3680
+ return [
3681
+ { cmd: "wl-copy", args: [] },
3682
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
3683
+ { cmd: "xsel", args: ["--clipboard", "--input"] }
3684
+ ];
3685
+ }
3686
+ function copyToClipboard(text, platform = process.platform) {
3687
+ for (const { cmd, args } of clipboardTools(platform)) {
3688
+ try {
3689
+ const res = spawnSync(cmd, args, {
3690
+ input: text,
3691
+ stdio: ["pipe", "ignore", "ignore"],
3692
+ timeout: 1e3
3693
+ });
3694
+ if (!res.error && res.status === 0) return true;
3695
+ } catch {
3696
+ }
3697
+ }
3698
+ return false;
3699
+ }
3700
+
3575
3701
  // src/cache.ts
3576
3702
  import {
3577
3703
  closeSync,
@@ -4319,6 +4445,165 @@ async function cmdMarkets(target, team, ctx) {
4319
4445
  out(disclaimer(t, c));
4320
4446
  out(c.dim(MARKET_INFO));
4321
4447
  }
4448
+ function pickShareStyle(v) {
4449
+ return v === "compact" ? "compact" : "social";
4450
+ }
4451
+ async function reliableShareSignals(ctx, matches) {
4452
+ const raw = await reliableMarketSignals(ctx, matches);
4453
+ const out2 = /* @__PURE__ */ new Map();
4454
+ for (const [id, s] of raw) if (marketDisplayable(s)) out2.set(id, s);
4455
+ return out2;
4456
+ }
4457
+ function emitShare(ctx, e, copy) {
4458
+ const snippet = formatShareSnippet(e.input, e.options);
4459
+ if (ctx.cfg.json) {
4460
+ emitJson({
4461
+ kind: e.kind,
4462
+ target: e.target,
4463
+ ...e.team ? { team: e.team } : {},
4464
+ source: e.input.source ?? null,
4465
+ informationalOnly: true,
4466
+ style: e.options.style ?? "social",
4467
+ snippet,
4468
+ matches: e.input.matches,
4469
+ marketSignals: Object.fromEntries(e.input.marketSignals ?? /* @__PURE__ */ new Map())
4470
+ });
4471
+ } else {
4472
+ out(snippet);
4473
+ }
4474
+ if (copy) {
4475
+ const ok = (ctx.copy ?? copyToClipboard)(snippet);
4476
+ process.stderr.write(
4477
+ (ok ? "Copied share snippet to clipboard." : "Clipboard unavailable; printed snippet instead.") + "\n"
4478
+ );
4479
+ }
4480
+ }
4481
+ async function cmdShare(target, team, opts, ctx) {
4482
+ const { cfg, t } = ctx;
4483
+ const baseOptions = {
4484
+ style: pickShareStyle(opts.style),
4485
+ includeMarkets: cfg.markets !== false,
4486
+ includeHashtag: opts.hashtag !== false,
4487
+ includeInstallLine: opts.installLine !== false
4488
+ };
4489
+ const copy = opts.copy === true;
4490
+ if (target === "live") {
4491
+ precheck(cfg, t);
4492
+ const { matches, source: source2 } = await getLiveMatches(adapterFor(ctx));
4493
+ emitShare(
4494
+ ctx,
4495
+ {
4496
+ kind: "live",
4497
+ target: "live",
4498
+ input: {
4499
+ title: "Live match pulse",
4500
+ matches,
4501
+ source: source2,
4502
+ emptyNote: "No matches in play right now.",
4503
+ installLine: "npx @claudinho/cli live",
4504
+ tz: cfg.tz,
4505
+ locale: cfg.lang
4506
+ },
4507
+ // Live snippets stay lean: no market enrichment (and no extra fetch).
4508
+ options: { ...baseOptions, includeMarkets: false }
4509
+ },
4510
+ copy
4511
+ );
4512
+ return;
4513
+ }
4514
+ if (target === "next") {
4515
+ precheck(cfg, t);
4516
+ if (!team) throw new InputError("Usage: claudinho share next <team>");
4517
+ const code = team.toUpperCase();
4518
+ const fixture = nextFixtureForTeam(code);
4519
+ const matches = fixture ? [fixture] : [];
4520
+ const signals2 = await reliableShareSignals(ctx, matches);
4521
+ const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
4522
+ emitShare(
4523
+ ctx,
4524
+ {
4525
+ kind: "next",
4526
+ target: "next",
4527
+ team: code,
4528
+ input: {
4529
+ title: `Next up for ${teamName}`,
4530
+ matches,
4531
+ marketSignals: signals2,
4532
+ emptyNote: `No upcoming fixture found for ${code}.`,
4533
+ installLine: `npx @claudinho/cli next ${code}`,
4534
+ tz: cfg.tz,
4535
+ locale: cfg.lang
4536
+ },
4537
+ options: baseOptions
4538
+ },
4539
+ copy
4540
+ );
4541
+ return;
4542
+ }
4543
+ if (target && target !== "today" && !isValidDate(target)) {
4544
+ precheck(cfg, t);
4545
+ const adapter = adapterFor(ctx);
4546
+ let match = allFixtures().find((m) => m.id === target);
4547
+ let source2;
4548
+ try {
4549
+ if (match) {
4550
+ const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
4551
+ match = live.find((m) => m.id === target) ?? match;
4552
+ source2 = adapter.name;
4553
+ }
4554
+ } catch {
4555
+ }
4556
+ const matches = match ? [match] : [];
4557
+ const signals2 = await reliableShareSignals(ctx, matches);
4558
+ emitShare(
4559
+ ctx,
4560
+ {
4561
+ kind: "match",
4562
+ target,
4563
+ input: {
4564
+ title: "Match pulse",
4565
+ matches,
4566
+ marketSignals: signals2,
4567
+ source: source2,
4568
+ emptyNote: `No match found with id ${target}.`,
4569
+ installLine: `npx @claudinho/cli match ${target}`,
4570
+ tz: cfg.tz,
4571
+ locale: cfg.lang
4572
+ },
4573
+ options: baseOptions
4574
+ },
4575
+ copy
4576
+ );
4577
+ return;
4578
+ }
4579
+ const explicitDate = target && target !== "today" ? target : void 0;
4580
+ precheck(cfg, t, explicitDate);
4581
+ const date = explicitDate ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
4582
+ const { matches: all, source } = await getMatchesForDate(adapterFor(ctx), date);
4583
+ const todays = fixturesByDate(date, all, cfg.tz);
4584
+ const signals = await reliableShareSignals(ctx, todays);
4585
+ const human = formatDate(`${date}T12:00:00.000Z`, { tz: cfg.tz, locale: cfg.lang });
4586
+ const title = explicitDate ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`;
4587
+ emitShare(
4588
+ ctx,
4589
+ {
4590
+ kind: "today",
4591
+ target: date,
4592
+ input: {
4593
+ title,
4594
+ matches: todays,
4595
+ marketSignals: signals,
4596
+ source,
4597
+ emptyNote: `No matches scheduled for ${human}.`,
4598
+ installLine: "npx @claudinho/cli today",
4599
+ tz: cfg.tz,
4600
+ locale: cfg.lang
4601
+ },
4602
+ options: baseOptions
4603
+ },
4604
+ copy
4605
+ );
4606
+ }
4322
4607
  var VIBES = [
4323
4608
  "Shipping code, watching goals.",
4324
4609
  "Green tests, green pitch.",
@@ -4352,7 +4637,7 @@ function handlePipeError(stream) {
4352
4637
  }
4353
4638
  handlePipeError(process.stdout);
4354
4639
  handlePipeError(process.stderr);
4355
- var VERSION = "0.3.0";
4640
+ var VERSION = "0.4.1";
4356
4641
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
4357
4642
  function ctxFrom(cmd) {
4358
4643
  const root = cmd.parent ?? cmd;
@@ -4411,6 +4696,13 @@ program.command("markets").description("show prediction-market signals (read-onl
4411
4696
  fail(e);
4412
4697
  }
4413
4698
  });
4699
+ 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" (e.g. MEX)').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) => {
4700
+ try {
4701
+ await cmdShare(target, team, opts, ctxFrom(cmd));
4702
+ } catch (e) {
4703
+ fail(e);
4704
+ }
4705
+ });
4414
4706
  program.command("prompt").description("print a one-line status (Claude Code statusline, tmux, Starship, \u2026)").action((_opts, cmd) => {
4415
4707
  cmdPrompt(ctxFrom(cmd));
4416
4708
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Claudinho CLI — the 2026 men's football tournament in your terminal: live scores, fixtures, group tables, prediction-market odds. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,7 @@
48
48
  "tsup": "^8.0.0",
49
49
  "typescript": "^5.7.0",
50
50
  "vitest": "^4.1.0",
51
- "@claudinho/core": "0.3.0"
51
+ "@claudinho/core": "0.4.1"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsup",