@nycpickleball/cli 1.1.0 → 1.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 (2) hide show
  1. package/dist/index.js +233 -3
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command as Command2 } from "commander";
5
5
 
6
6
  // src/commands/league.ts
7
- import { readFileSync } from "fs";
7
+ import { readFileSync as readFileSync2 } from "fs";
8
8
  import { Option } from "commander";
9
9
 
10
10
  // src/api.ts
@@ -108,6 +108,123 @@ function dieWith(error) {
108
108
  process.exit(1);
109
109
  }
110
110
 
111
+ // src/sheet-parser.ts
112
+ import { readFileSync } from "fs";
113
+ import * as XLSX from "xlsx";
114
+ var PLAYER_NAME_HEADER = "Player Name";
115
+ var GAME_COUNT = 5;
116
+ function parseWorkbook(path) {
117
+ const buf = readFileSync(path);
118
+ const wb = XLSX.read(buf, { type: "buffer" });
119
+ const weeks = [];
120
+ for (const sheetName of wb.SheetNames) {
121
+ const match = /^W(\d+)\s+Dashboard$/i.exec(sheetName);
122
+ if (!match) continue;
123
+ const weekNumber = parseInt(match[1], 10);
124
+ if (!Number.isInteger(weekNumber)) continue;
125
+ const ws = wb.Sheets[sheetName];
126
+ const rows = XLSX.utils.sheet_to_json(ws, {
127
+ header: 1,
128
+ defval: null,
129
+ blankrows: true
130
+ });
131
+ const courts = extractCourts(rows);
132
+ weeks.push({ weekNumber, courts });
133
+ }
134
+ return weeks;
135
+ }
136
+ function extractCourts(rows) {
137
+ const courts = [];
138
+ let i = 0;
139
+ while (i < rows.length) {
140
+ const row = rows[i] ?? [];
141
+ const courtNum = row[0];
142
+ const courtRules = row[1];
143
+ if ((typeof courtNum === "number" || /^\d+$/.test(String(courtNum ?? ""))) && typeof courtRules === "string" && /games to/i.test(courtRules)) {
144
+ const num = typeof courtNum === "number" ? courtNum : parseInt(String(courtNum), 10);
145
+ const headerRow = rows[i + 1] ?? [];
146
+ if (headerRow[1] !== PLAYER_NAME_HEADER) {
147
+ i++;
148
+ continue;
149
+ }
150
+ const players = [];
151
+ const rawScores = [];
152
+ let r = i + 2;
153
+ while (r < rows.length && players.length < 5) {
154
+ const playerRow = rows[r] ?? [];
155
+ const name = playerRow[1];
156
+ if (typeof name !== "string" || !name.trim()) break;
157
+ const scores = [];
158
+ for (let g = 0; g < GAME_COUNT; g++) {
159
+ const v = playerRow[2 + g];
160
+ if (v === null || v === "" || v === void 0) {
161
+ scores.push(null);
162
+ } else if (typeof v === "number") {
163
+ scores.push(v);
164
+ } else {
165
+ const n = Number(v);
166
+ scores.push(Number.isFinite(n) ? n : null);
167
+ }
168
+ }
169
+ const duprRaw = playerRow[15];
170
+ const avgDupr = typeof duprRaw === "number" && Number.isFinite(duprRaw) ? duprRaw : void 0;
171
+ players.push({ name: name.trim(), avgDupr });
172
+ rawScores.push(scores);
173
+ r++;
174
+ }
175
+ if (players.length === 0) {
176
+ i = r + 1;
177
+ continue;
178
+ }
179
+ const gameToScoreMatch = /games to\s*(\d+)/i.exec(courtRules);
180
+ const gameToScore = gameToScoreMatch ? parseInt(gameToScoreMatch[1], 10) : 15;
181
+ const games = deriveGames(num, rawScores);
182
+ courts.push({
183
+ name: `Court ${num}`,
184
+ rules: courtRules.trim(),
185
+ gameToScore,
186
+ players,
187
+ games
188
+ });
189
+ i = r;
190
+ } else {
191
+ i++;
192
+ }
193
+ }
194
+ return courts;
195
+ }
196
+ function deriveGames(courtNum, rawScores) {
197
+ const out = [];
198
+ if (rawScores.length === 0) return out;
199
+ const gameCount = rawScores[0].length;
200
+ for (let g = 0; g < gameCount; g++) {
201
+ const playerScores = rawScores.map((row) => row[g]).filter((s) => s !== null && s !== void 0);
202
+ if (playerScores.length === 0) {
203
+ continue;
204
+ }
205
+ if (playerScores.every((s) => s === 0)) continue;
206
+ const distinct = Array.from(new Set(playerScores));
207
+ let team1Score = null;
208
+ let team2Score = null;
209
+ if (distinct.length === 1) {
210
+ team1Score = distinct[0];
211
+ team2Score = distinct[0];
212
+ } else if (distinct.length === 2) {
213
+ team1Score = distinct[0];
214
+ team2Score = distinct[1];
215
+ } else {
216
+ continue;
217
+ }
218
+ out.push({
219
+ courtName: `Court ${courtNum}`,
220
+ gameNumber: g + 1,
221
+ team1Score,
222
+ team2Score
223
+ });
224
+ }
225
+ return out;
226
+ }
227
+
111
228
  // src/commands/league.ts
112
229
  var LEAGUE_TYPES = ["TRIPLES", "LEVEL_TWO", "LEVEL_THREE", "LEVEL_FOUR"];
113
230
  var DAYS = [
@@ -261,7 +378,7 @@ function registerLeagueCommands(program2) {
261
378
  const { client } = resolveConfig(rootOpts(this));
262
379
  let parsed;
263
380
  try {
264
- const text = readFileSync(options.file, "utf8");
381
+ const text = readFileSync2(options.file, "utf8");
265
382
  parsed = JSON.parse(text);
266
383
  } catch (e) {
267
384
  dieWith(
@@ -381,6 +498,119 @@ function registerLeagueCommands(program2) {
381
498
  dieWith(e);
382
499
  }
383
500
  });
501
+ week.command("roster <slug> <weekNumber>").description("Set a per-week roster snapshot from a JSON file").requiredOption(
502
+ "--file <path>",
503
+ "Path to JSON with shape { courts: [{ name, players: [...] }] }"
504
+ ).action(async function(slug, weekNumber, options) {
505
+ const { client } = resolveConfig(rootOpts(this));
506
+ let parsed;
507
+ try {
508
+ const text = readFileSync2(options.file, "utf8");
509
+ parsed = JSON.parse(text);
510
+ } catch (e) {
511
+ dieWith(
512
+ new Error(
513
+ `Could not read or parse ${options.file}: ${e.message}`
514
+ )
515
+ );
516
+ }
517
+ try {
518
+ const res = await client.put(
519
+ `/api/league/${encodeURIComponent(slug)}/weeks/${encodeURIComponent(weekNumber)}/roster`,
520
+ parsed
521
+ );
522
+ printJson(res);
523
+ } catch (e) {
524
+ dieWith(e);
525
+ }
526
+ });
527
+ league.command("import-sheet <slug>").description(
528
+ "Import per-week rosters + scores from a Gotham-style xlsx workbook"
529
+ ).requiredOption("--file <path>", "Path to the .xlsx workbook").option(
530
+ "--week <n>",
531
+ "Only import a single week number (default: all weeks found)",
532
+ intOpt
533
+ ).option(
534
+ "--dry-run",
535
+ "Parse and report what would be uploaded without making any API calls"
536
+ ).action(async function(slug, options) {
537
+ const { client } = resolveConfig(rootOpts(this));
538
+ let weeks;
539
+ try {
540
+ weeks = parseWorkbook(options.file);
541
+ } catch (e) {
542
+ dieWith(
543
+ new Error(
544
+ `Failed to parse workbook ${options.file}: ${e.message}`
545
+ )
546
+ );
547
+ }
548
+ const filtered = typeof options.week === "number" ? weeks.filter((w) => w.weekNumber === options.week) : weeks;
549
+ if (filtered.length === 0) {
550
+ dieWith(
551
+ new Error(
552
+ options.week ? `No Wn Dashboard sheet found for week ${options.week}.` : "No Wn Dashboard sheets found in the workbook."
553
+ )
554
+ );
555
+ }
556
+ const rows = [];
557
+ for (const week2 of filtered) {
558
+ const courtCount = week2.courts.length;
559
+ const playerCount = week2.courts.reduce(
560
+ (acc, c) => acc + c.players.length,
561
+ 0
562
+ );
563
+ const gameCount = week2.courts.reduce(
564
+ (acc, c) => acc + c.games.length,
565
+ 0
566
+ );
567
+ const label = `W${week2.weekNumber}`;
568
+ if (options.dryRun) {
569
+ rows.push({
570
+ week: label,
571
+ status: `would upload ${courtCount} courts / ${playerCount} players / ${gameCount} games`
572
+ });
573
+ continue;
574
+ }
575
+ try {
576
+ await client.put(
577
+ `/api/league/${encodeURIComponent(slug)}/weeks/${week2.weekNumber}/roster`,
578
+ {
579
+ courts: week2.courts.map((c) => ({
580
+ name: c.name,
581
+ rules: c.rules,
582
+ gameToScore: c.gameToScore,
583
+ players: c.players.map((p) => ({
584
+ name: p.name,
585
+ avgDupr: p.avgDupr ?? null
586
+ }))
587
+ }))
588
+ }
589
+ );
590
+ if (gameCount > 0) {
591
+ await client.post(
592
+ `/api/league/${encodeURIComponent(slug)}/weeks/${week2.weekNumber}/games`,
593
+ {
594
+ games: week2.courts.flatMap((c) => c.games)
595
+ }
596
+ );
597
+ }
598
+ rows.push({
599
+ week: label,
600
+ status: `synced ${courtCount} courts, ${playerCount} players, ${gameCount} games`
601
+ });
602
+ } catch (e) {
603
+ rows.push({
604
+ week: label,
605
+ status: `error: ${e instanceof Error ? e.message : String(e)}`
606
+ });
607
+ }
608
+ }
609
+ printTable(rows, [
610
+ { key: "week", label: "WEEK" },
611
+ { key: "status", label: "STATUS" }
612
+ ]);
613
+ });
384
614
  game.command("import <slug> <weekNumber>").description("Bulk-record game scores from a JSON file").requiredOption(
385
615
  "--file <path>",
386
616
  "Path to JSON with shape { games: [{ courtName, gameNumber, team1Score, team2Score }] }"
@@ -388,7 +618,7 @@ function registerLeagueCommands(program2) {
388
618
  const { client } = resolveConfig(rootOpts(this));
389
619
  let parsed;
390
620
  try {
391
- const text = readFileSync(options.file, "utf8");
621
+ const text = readFileSync2(options.file, "utf8");
392
622
  parsed = JSON.parse(text);
393
623
  } catch (e) {
394
624
  dieWith(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nycpickleball/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for managing NYC Pickleball leagues via the deployed API.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -21,7 +21,8 @@
21
21
  "prepublishOnly": "pnpm build"
22
22
  },
23
23
  "dependencies": {
24
- "commander": "^12.1.0"
24
+ "commander": "^12.1.0",
25
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@semantic-release/changelog": "^6.0.3",