@nycpickleball/cli 1.1.1 → 1.2.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.
- package/dist/index.js +233 -3
- 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 =
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|