bbdata-cli 0.1.0 → 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/dist/bin/bbdata.js +128 -16
- package/dist/bin/bbdata.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +122 -12
- package/dist/src/index.js.map +1 -1
- package/dist/templates/queries/hitter-batted-ball.ts +66 -0
- package/dist/templates/queries/hitter-hot-cold-zones.ts +81 -0
- package/dist/templates/queries/hitter-vs-pitch-type.ts +78 -0
- package/dist/templates/queries/index.ts +24 -0
- package/dist/templates/queries/leaderboard-comparison.ts +72 -0
- package/dist/templates/queries/leaderboard-custom.ts +90 -0
- package/dist/templates/queries/matchup-pitcher-vs-hitter.ts +81 -0
- package/dist/templates/queries/matchup-situational.ts +68 -0
- package/dist/templates/queries/pitcher-arsenal.ts +89 -0
- package/dist/templates/queries/pitcher-handedness-splits.ts +81 -0
- package/dist/templates/queries/pitcher-velocity-trend.ts +73 -0
- package/dist/templates/queries/registry.ts +73 -0
- package/dist/templates/queries/trend-rolling-average.ts +86 -0
- package/dist/templates/queries/trend-year-over-year.ts +73 -0
- package/dist/templates/reports/advance-lineup.hbs +29 -0
- package/dist/templates/reports/advance-sp.hbs +60 -0
- package/dist/templates/reports/college-hitter-draft.hbs +49 -0
- package/dist/templates/reports/college-pitcher-draft.hbs +48 -0
- package/dist/templates/reports/dev-progress.hbs +29 -0
- package/dist/templates/reports/draft-board-card.hbs +35 -0
- package/dist/templates/reports/hs-prospect.hbs +48 -0
- package/dist/templates/reports/partials/footer.hbs +7 -0
- package/dist/templates/reports/partials/header.hbs +12 -0
- package/dist/templates/reports/post-promotion.hbs +25 -0
- package/dist/templates/reports/pro-hitter-eval.hbs +65 -0
- package/dist/templates/reports/pro-pitcher-eval.hbs +69 -0
- package/dist/templates/reports/registry.ts +215 -0
- package/dist/templates/reports/relief-pitcher-quick.hbs +29 -0
- package/dist/templates/reports/trade-target-onepager.hbs +45 -0
- package/package.json +1 -1
package/dist/bin/bbdata.js
CHANGED
|
@@ -165,8 +165,8 @@ var SavantAdapter = class {
|
|
|
165
165
|
}
|
|
166
166
|
const params = new URLSearchParams({
|
|
167
167
|
all: "true",
|
|
168
|
-
type: "
|
|
169
|
-
...query2.stat_type === "pitching" ? { player_type: "pitcher", pitchers_lookup: playerId ?? "" } : { player_type: "batter", batters_lookup: playerId ?? "" },
|
|
168
|
+
type: "details",
|
|
169
|
+
...query2.stat_type === "pitching" ? { player_type: "pitcher", "pitchers_lookup[]": playerId ?? "" } : { player_type: "batter", "batters_lookup[]": playerId ?? "" },
|
|
170
170
|
...query2.start_date ? { game_date_gt: query2.start_date } : { game_date_gt: `${query2.season}-01-01` },
|
|
171
171
|
...query2.end_date ? { game_date_lt: query2.end_date } : { game_date_lt: `${query2.season}-12-31` }
|
|
172
172
|
});
|
|
@@ -192,6 +192,9 @@ var SavantAdapter = class {
|
|
|
192
192
|
cast: true,
|
|
193
193
|
cast_date: false
|
|
194
194
|
});
|
|
195
|
+
if (rawRows.length === 0 && csvText.trim().length > 50) {
|
|
196
|
+
log.debug("Savant returned headers but no data rows");
|
|
197
|
+
}
|
|
195
198
|
const filteredRows = rawRows.filter(
|
|
196
199
|
(row) => row.pitch_type && String(row.pitch_type).trim() !== ""
|
|
197
200
|
);
|
|
@@ -231,6 +234,7 @@ var SavantAdapter = class {
|
|
|
231
234
|
|
|
232
235
|
// src/adapters/fangraphs.ts
|
|
233
236
|
var FG_LEADERS_URL = "https://www.fangraphs.com/api/leaders/major-league/data";
|
|
237
|
+
var stripHtml = (s) => s.replace(/<[^>]*>/g, "");
|
|
234
238
|
var FanGraphsAdapter = class {
|
|
235
239
|
source = "fangraphs";
|
|
236
240
|
description = "FanGraphs \u2014 aggregated stats, leaderboards, WAR, wRC+, FIP";
|
|
@@ -259,7 +263,7 @@ var FanGraphsAdapter = class {
|
|
|
259
263
|
const parsed = JSON.parse(data);
|
|
260
264
|
const nameNorm = name.toLowerCase().trim();
|
|
261
265
|
const players = parsed.data ?? [];
|
|
262
|
-
const getName = (p) => String(p.PlayerName ?? p.Name ?? "").toLowerCase().trim();
|
|
266
|
+
const getName = (p) => stripHtml(String(p.PlayerName ?? p.Name ?? "")).toLowerCase().trim();
|
|
263
267
|
let match = players.find((p) => getName(p) === nameNorm);
|
|
264
268
|
if (!match) {
|
|
265
269
|
const tokens = nameNorm.split(/\s+/);
|
|
@@ -275,8 +279,8 @@ var FanGraphsAdapter = class {
|
|
|
275
279
|
return {
|
|
276
280
|
mlbam_id: String(match.xMLBAMID ?? ""),
|
|
277
281
|
fangraphs_id: String(match.playerid ?? ""),
|
|
278
|
-
name: String(match.PlayerName ?? match.Name),
|
|
279
|
-
team: String(match.Team ?? "")
|
|
282
|
+
name: stripHtml(String(match.PlayerName ?? match.Name)),
|
|
283
|
+
team: stripHtml(String(match.TeamNameAbb ?? match.TeamName ?? match.Team ?? ""))
|
|
280
284
|
};
|
|
281
285
|
} catch (error) {
|
|
282
286
|
log.warn(`FanGraphs player resolution failed: ${error}`);
|
|
@@ -308,7 +312,7 @@ var FanGraphsAdapter = class {
|
|
|
308
312
|
let filtered = rows;
|
|
309
313
|
if (query2.player_name) {
|
|
310
314
|
const nameNorm = query2.player_name.toLowerCase().trim();
|
|
311
|
-
const getName = (r) => String(r.PlayerName ?? r.Name ?? "").toLowerCase().trim();
|
|
315
|
+
const getName = (r) => stripHtml(String(r.PlayerName ?? r.Name ?? "")).toLowerCase().trim();
|
|
312
316
|
const exact = rows.filter((r) => getName(r) === nameNorm);
|
|
313
317
|
if (exact.length > 0) {
|
|
314
318
|
filtered = exact;
|
|
@@ -322,8 +326,8 @@ var FanGraphsAdapter = class {
|
|
|
322
326
|
}
|
|
323
327
|
const stats = filtered.map((row) => ({
|
|
324
328
|
player_id: String(row.xMLBAMID ?? row.playerid ?? ""),
|
|
325
|
-
player_name: String(row.PlayerName ?? row.Name ?? ""),
|
|
326
|
-
team: String(row.Team ?? ""),
|
|
329
|
+
player_name: stripHtml(String(row.PlayerName ?? row.Name ?? "")),
|
|
330
|
+
team: stripHtml(String(row.TeamNameAbb ?? row.TeamName ?? row.Team ?? "")),
|
|
327
331
|
season: query2.season,
|
|
328
332
|
stat_type: query2.stat_type,
|
|
329
333
|
stats: row
|
|
@@ -397,13 +401,88 @@ var BaseballReferenceAdapter = class {
|
|
|
397
401
|
}
|
|
398
402
|
};
|
|
399
403
|
|
|
404
|
+
// src/adapters/stdin.ts
|
|
405
|
+
var StdinAdapter = class {
|
|
406
|
+
source = "stdin";
|
|
407
|
+
description = "Local data from stdin (for sandboxed environments)";
|
|
408
|
+
data = [];
|
|
409
|
+
player = null;
|
|
410
|
+
loaded = false;
|
|
411
|
+
/**
|
|
412
|
+
* Load data from a pre-read stdin string.
|
|
413
|
+
* Called by the CLI before the adapter is used.
|
|
414
|
+
*/
|
|
415
|
+
load(raw) {
|
|
416
|
+
try {
|
|
417
|
+
const parsed = JSON.parse(raw);
|
|
418
|
+
if (Array.isArray(parsed)) {
|
|
419
|
+
this.data = parsed;
|
|
420
|
+
} else if (parsed.data && Array.isArray(parsed.data)) {
|
|
421
|
+
this.data = parsed.data;
|
|
422
|
+
if (parsed.player) {
|
|
423
|
+
this.player = parsed.player;
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
throw new Error('Expected JSON array or { "data": [...] } object');
|
|
427
|
+
}
|
|
428
|
+
this.loaded = true;
|
|
429
|
+
log.info(`Stdin adapter loaded ${this.data.length} records`);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Failed to parse stdin data: ${error instanceof Error ? error.message : String(error)}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
supports(_query) {
|
|
437
|
+
return this.loaded && this.data.length > 0;
|
|
438
|
+
}
|
|
439
|
+
async fetch(query2) {
|
|
440
|
+
if (!this.loaded) {
|
|
441
|
+
throw new Error("Stdin adapter has no data \u2014 pipe JSON via stdin with --stdin flag");
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
data: this.data,
|
|
445
|
+
source: "stdin",
|
|
446
|
+
cached: false,
|
|
447
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
448
|
+
meta: {
|
|
449
|
+
rowCount: this.data.length,
|
|
450
|
+
season: query2.season,
|
|
451
|
+
query: query2
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async resolvePlayer(name) {
|
|
456
|
+
if (this.player && this.player.name.toLowerCase() === name.toLowerCase()) {
|
|
457
|
+
return this.player;
|
|
458
|
+
}
|
|
459
|
+
const firstRecord = this.data[0];
|
|
460
|
+
if (firstRecord) {
|
|
461
|
+
const id = firstRecord.pitcher_id ?? firstRecord.player_id ?? "";
|
|
462
|
+
const recordName = firstRecord.pitcher_name ?? firstRecord.player_name ?? name;
|
|
463
|
+
if (id) {
|
|
464
|
+
return {
|
|
465
|
+
mlbam_id: id,
|
|
466
|
+
name: recordName
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
400
474
|
// src/adapters/index.ts
|
|
475
|
+
var stdinAdapter = new StdinAdapter();
|
|
401
476
|
var adapters = {
|
|
402
477
|
"mlb-stats-api": new MlbStatsApiAdapter(),
|
|
403
478
|
"savant": new SavantAdapter(),
|
|
404
479
|
"fangraphs": new FanGraphsAdapter(),
|
|
405
|
-
"baseball-reference": new BaseballReferenceAdapter()
|
|
480
|
+
"baseball-reference": new BaseballReferenceAdapter(),
|
|
481
|
+
"stdin": stdinAdapter
|
|
406
482
|
};
|
|
483
|
+
function getStdinAdapter() {
|
|
484
|
+
return stdinAdapter;
|
|
485
|
+
}
|
|
407
486
|
function resolveAdapters(preferred) {
|
|
408
487
|
return preferred.map((source) => adapters[source]).filter(Boolean);
|
|
409
488
|
}
|
|
@@ -574,6 +653,20 @@ function getTemplatesDir() {
|
|
|
574
653
|
return dir;
|
|
575
654
|
}
|
|
576
655
|
|
|
656
|
+
// src/utils/stdin.ts
|
|
657
|
+
function readStdin() {
|
|
658
|
+
return new Promise((resolve, reject) => {
|
|
659
|
+
if (process.stdin.isTTY) {
|
|
660
|
+
reject(new Error(`--stdin flag requires piped input. Usage: echo '{"data":[...]}' | bbdata query ... --stdin`));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const chunks = [];
|
|
664
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
665
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
666
|
+
process.stdin.on("error", reject);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
577
670
|
// src/templates/queries/registry.ts
|
|
578
671
|
var templates = /* @__PURE__ */ new Map();
|
|
579
672
|
function registerTemplate(template13) {
|
|
@@ -1174,11 +1267,12 @@ var template9 = {
|
|
|
1174
1267
|
"bbdata query leaderboard-custom --stat ERA --min-ip 100 --top 10 --format table"
|
|
1175
1268
|
],
|
|
1176
1269
|
buildQuery(params) {
|
|
1270
|
+
const pitchingStats = ["era", "fip", "xfip", "siera", "whip", "k/9", "bb/9", "hr/9", "ip", "w", "sv", "hld"];
|
|
1271
|
+
const isPitching = pitchingStats.includes((params.stat ?? "").toLowerCase());
|
|
1177
1272
|
return {
|
|
1178
1273
|
season: params.season ?? (/* @__PURE__ */ new Date()).getFullYear(),
|
|
1179
1274
|
team: params.team,
|
|
1180
|
-
stat_type: "batting",
|
|
1181
|
-
// Will be overridden based on stat
|
|
1275
|
+
stat_type: isPitching ? "pitching" : "batting",
|
|
1182
1276
|
min_pa: params.minPa,
|
|
1183
1277
|
min_ip: params.minIp
|
|
1184
1278
|
};
|
|
@@ -1424,6 +1518,12 @@ registerTemplate(template12);
|
|
|
1424
1518
|
|
|
1425
1519
|
// src/commands/query.ts
|
|
1426
1520
|
async function query(options) {
|
|
1521
|
+
if (options.stdin) {
|
|
1522
|
+
const raw = await readStdin();
|
|
1523
|
+
const adapter = getStdinAdapter();
|
|
1524
|
+
adapter.load(raw);
|
|
1525
|
+
options.source = "stdin";
|
|
1526
|
+
}
|
|
1427
1527
|
const config = getConfig();
|
|
1428
1528
|
const outputFormat = options.format ?? config.defaultFormat;
|
|
1429
1529
|
const template13 = getTemplate(options.template);
|
|
@@ -1464,6 +1564,10 @@ ${available}`);
|
|
|
1464
1564
|
});
|
|
1465
1565
|
const rows = template13.transform(adapterResult.data, params);
|
|
1466
1566
|
const columns = template13.columns(params);
|
|
1567
|
+
if (rows.length === 0) {
|
|
1568
|
+
log.debug(`${adapter.source} returned 0 rows. Trying next source...`);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1467
1571
|
result = {
|
|
1468
1572
|
rows,
|
|
1469
1573
|
columns,
|
|
@@ -1503,7 +1607,7 @@ ${available}`);
|
|
|
1503
1607
|
};
|
|
1504
1608
|
}
|
|
1505
1609
|
function registerQueryCommand(program2) {
|
|
1506
|
-
const cmd = program2.command("query [template]").description("Query baseball data using pre-built templates").option("-p, --player <name>", "Player name").option("--players <names>", "Comma-separated player names (for matchups/comparisons)").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-f, --format <fmt>", "Output: json, table, csv, markdown", "json").option("--source <src>", "Force a data source: savant, fangraphs, mlb-stats-api").option("--stat <stat>", "Stat to query (for leaderboards)").option("--pitch-type <type>", "Filter by pitch type (e.g., FF, SL)").option("--min-pa <n>", "Minimum plate appearances", parseInt).option("--min-ip <n>", "Minimum innings pitched", parseInt).option("--top <n>", "Number of results for leaderboards", parseInt).option("--seasons <range>", "Season range (e.g., 2023-2025)").option("--no-cache", "Bypass cache").addHelpText("after", `
|
|
1610
|
+
const cmd = program2.command("query [template]").description("Query baseball data using pre-built templates").option("-p, --player <name>", "Player name").option("--players <names>", "Comma-separated player names (for matchups/comparisons)").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-f, --format <fmt>", "Output: json, table, csv, markdown", "json").option("--source <src>", "Force a data source: savant, fangraphs, mlb-stats-api").option("--stat <stat>", "Stat to query (for leaderboards)").option("--pitch-type <type>", "Filter by pitch type (e.g., FF, SL)").option("--min-pa <n>", "Minimum plate appearances", parseInt).option("--min-ip <n>", "Minimum innings pitched", parseInt).option("--top <n>", "Number of results for leaderboards", parseInt).option("--seasons <range>", "Season range (e.g., 2023-2025)").option("--no-cache", "Bypass cache").option("--stdin", "Read pre-fetched JSON data from stdin instead of fetching from APIs").addHelpText("after", `
|
|
1507
1611
|
Examples:
|
|
1508
1612
|
bbdata query pitcher-arsenal --player "Corbin Burnes" --season 2025
|
|
1509
1613
|
bbdata query hitter-batted-ball --player "Aaron Judge" --format table
|
|
@@ -1542,7 +1646,8 @@ Available templates:
|
|
|
1542
1646
|
seasons: opts.seasons,
|
|
1543
1647
|
format: opts.format,
|
|
1544
1648
|
source: opts.source,
|
|
1545
|
-
cache: opts.cache
|
|
1649
|
+
cache: opts.cache,
|
|
1650
|
+
stdin: opts.stdin
|
|
1546
1651
|
});
|
|
1547
1652
|
log.data(result.formatted);
|
|
1548
1653
|
} catch (error) {
|
|
@@ -1807,6 +1912,11 @@ ${sections}
|
|
|
1807
1912
|
`;
|
|
1808
1913
|
}
|
|
1809
1914
|
async function report(options) {
|
|
1915
|
+
if (options.stdin) {
|
|
1916
|
+
const raw = await readStdin();
|
|
1917
|
+
const adapter = getStdinAdapter();
|
|
1918
|
+
adapter.load(raw);
|
|
1919
|
+
}
|
|
1810
1920
|
const config = getConfig();
|
|
1811
1921
|
const audience = options.audience ?? config.defaultAudience;
|
|
1812
1922
|
const template13 = getReportTemplate(options.template);
|
|
@@ -1826,7 +1936,8 @@ ${available}`);
|
|
|
1826
1936
|
player: options.player,
|
|
1827
1937
|
team: options.team,
|
|
1828
1938
|
season,
|
|
1829
|
-
format: "json"
|
|
1939
|
+
format: "json",
|
|
1940
|
+
...options.stdin ? { source: "stdin" } : {}
|
|
1830
1941
|
});
|
|
1831
1942
|
dataResults[req.queryTemplate] = result.data;
|
|
1832
1943
|
if (!dataSources.includes(result.meta.source)) {
|
|
@@ -1893,7 +2004,7 @@ function validateReport(content, requiredSections) {
|
|
|
1893
2004
|
};
|
|
1894
2005
|
}
|
|
1895
2006
|
function registerReportCommand(program2) {
|
|
1896
|
-
program2.command("report [template]").description("Generate scouting reports using pre-built templates").option("-p, --player <name>", "Player name").option("-t, --team <code>", "Team abbreviation").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-a, --audience <role>", "Target audience: coach, gm, scout, analyst").option("-f, --format <fmt>", "Output: markdown, json", "markdown").option("--validate", "Run validation checklist on the report").addHelpText("after", `
|
|
2007
|
+
program2.command("report [template]").description("Generate scouting reports using pre-built templates").option("-p, --player <name>", "Player name").option("-t, --team <code>", "Team abbreviation").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-a, --audience <role>", "Target audience: coach, gm, scout, analyst").option("-f, --format <fmt>", "Output: markdown, json", "markdown").option("--validate", "Run validation checklist on the report").option("--stdin", "Read pre-fetched JSON data from stdin instead of fetching from APIs").addHelpText("after", `
|
|
1897
2008
|
Examples:
|
|
1898
2009
|
bbdata report pro-pitcher-eval --player "Corbin Burnes"
|
|
1899
2010
|
bbdata report advance-sp --player "Gerrit Cole" --audience coach --validate
|
|
@@ -1924,7 +2035,8 @@ Available templates:
|
|
|
1924
2035
|
season: opts.season ? parseInt(opts.season) : void 0,
|
|
1925
2036
|
audience: opts.audience,
|
|
1926
2037
|
format: opts.format,
|
|
1927
|
-
validate: opts.validate
|
|
2038
|
+
validate: opts.validate,
|
|
2039
|
+
stdin: opts.stdin
|
|
1928
2040
|
});
|
|
1929
2041
|
log.data(result.formatted);
|
|
1930
2042
|
if (result.validation && !result.validation.passed) {
|