@nycpickleball/cli 1.3.0 → 1.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 (2) hide show
  1. package/dist/index.js +217 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command2 } from "commander";
4
+ import { Command as Command3 } from "commander";
5
5
 
6
6
  // src/commands/league.ts
7
7
  import { readFileSync as readFileSync2 } from "fs";
@@ -66,9 +66,9 @@ function createClient(opts) {
66
66
 
67
67
  // src/config.ts
68
68
  var DEFAULT_BASE_URL = "https://nycpickleball.vercel.app";
69
- function resolveConfig(rootOpts2) {
70
- const baseUrl = (rootOpts2.url || process.env.NYCP_API_URL || DEFAULT_BASE_URL).trim();
71
- const token = (rootOpts2.token || process.env.NYCP_API_KEY || "").trim();
69
+ function resolveConfig(rootOpts3) {
70
+ const baseUrl = (rootOpts3.url || process.env.NYCP_API_URL || DEFAULT_BASE_URL).trim();
71
+ const token = (rootOpts3.token || process.env.NYCP_API_KEY || "").trim();
72
72
  const client = createClient({ baseUrl, token });
73
73
  return { baseUrl, token, client };
74
74
  }
@@ -639,13 +639,225 @@ function registerLeagueCommands(program2) {
639
639
  });
640
640
  }
641
641
 
642
+ // src/commands/player.ts
643
+ import "commander";
644
+
645
+ // src/questionnaire-parser.ts
646
+ import { readFileSync as readFileSync3 } from "fs";
647
+ import * as XLSX2 from "xlsx";
648
+ var HEADER_ALIASES = {
649
+ "email address": "email",
650
+ email: "email",
651
+ "full name as you want it to appear in the leaderboard": "name",
652
+ "full name": "name",
653
+ name: "name",
654
+ "self-rating level (be as granular as possible, ie. 2.85, etc.)": "selfRating",
655
+ "self-rating": "selfRating",
656
+ "what is your dupr rating if you have one? provide the actual number (ie, 3.43)": "duprRating",
657
+ "dupr rating": "duprRating",
658
+ "what is your 6 digit dupr id?": "duprId",
659
+ "dupr id": "duprId",
660
+ "what is your instagram handle so we can tag you in any photos or video?": "instagram",
661
+ instagram: "instagram",
662
+ "i confirm that i have joined the nyc pickleball slack": "slackJoined",
663
+ "which level are you participating in?": "level",
664
+ level: "level"
665
+ };
666
+ function parseNumber(v) {
667
+ if (typeof v === "number" && Number.isFinite(v)) return v;
668
+ if (typeof v === "string") {
669
+ const n = parseFloat(v);
670
+ if (Number.isFinite(n)) return n;
671
+ }
672
+ return void 0;
673
+ }
674
+ function parseSlackJoined(v) {
675
+ if (typeof v !== "string") return void 0;
676
+ const t = v.trim().toLowerCase();
677
+ if (t.startsWith("yes")) return true;
678
+ if (t.startsWith("no")) return false;
679
+ return void 0;
680
+ }
681
+ function parseQuestionnaire(path) {
682
+ const buf = readFileSync3(path);
683
+ const wb = XLSX2.read(buf, { type: "buffer" });
684
+ const sheet = wb.Sheets[wb.SheetNames[0]];
685
+ const rows = XLSX2.utils.sheet_to_json(sheet, {
686
+ header: 1,
687
+ defval: null,
688
+ blankrows: true
689
+ });
690
+ if (rows.length < 2) return [];
691
+ const header = rows[0].map(
692
+ (h) => typeof h === "string" ? h.trim().toLowerCase() : ""
693
+ );
694
+ const fieldMap = header.map(
695
+ (h) => HEADER_ALIASES[h] ?? "skip"
696
+ );
697
+ const out = [];
698
+ for (let r = 1; r < rows.length; r++) {
699
+ const row = rows[r];
700
+ if (!row || row.every((c) => c === null || c === "")) continue;
701
+ const draft = {};
702
+ for (let c = 0; c < fieldMap.length; c++) {
703
+ const field = fieldMap[c];
704
+ if (field === "skip") continue;
705
+ const value = row[c];
706
+ if (value === null || value === void 0 || value === "") continue;
707
+ if (field === "selfRating" || field === "duprRating") {
708
+ draft[field] = parseNumber(value);
709
+ } else if (field === "slackJoined") {
710
+ draft[field] = parseSlackJoined(value);
711
+ } else if (field === "email" && typeof value === "string") {
712
+ draft[field] = value.trim().toLowerCase();
713
+ } else if (typeof value === "string") {
714
+ draft[field] = value.trim();
715
+ }
716
+ }
717
+ if (!draft.email || !draft.name) continue;
718
+ out.push(draft);
719
+ }
720
+ return out;
721
+ }
722
+
723
+ // src/commands/player.ts
724
+ function rootOpts2(cmd) {
725
+ let c = cmd;
726
+ while (c && c.parent) c = c.parent;
727
+ return c?.opts() ?? {};
728
+ }
729
+ function registerPlayerCommands(program2) {
730
+ const player = program2.command("player").description("Manage players");
731
+ player.command("list").description("List all players").action(async function() {
732
+ const { client } = resolveConfig(rootOpts2(this));
733
+ try {
734
+ const res = await client.get("/api/players");
735
+ printTable(
736
+ res.data.map((p) => ({
737
+ name: p.name,
738
+ email: p.email,
739
+ dupr: p.duprRating?.toFixed(2) ?? "\u2014",
740
+ linked: p.workosUserId ? "yes" : "no"
741
+ })),
742
+ [
743
+ { key: "name", label: "NAME" },
744
+ { key: "email", label: "EMAIL" },
745
+ { key: "dupr", label: "DUPR" },
746
+ { key: "linked", label: "LINKED" }
747
+ ]
748
+ );
749
+ } catch (e) {
750
+ dieWith(e);
751
+ }
752
+ });
753
+ player.command("import").description(
754
+ "Bulk upsert players from a Google Forms-style questionnaire xlsx"
755
+ ).requiredOption("--file <path>", "Path to the .xlsx questionnaire").option(
756
+ "--level <substring>",
757
+ "Only import rows whose 'level' column contains this substring (case-insensitive)"
758
+ ).option(
759
+ "--dry-run",
760
+ "Parse and report what would be uploaded without making any API calls"
761
+ ).action(async function(options) {
762
+ const { client } = resolveConfig(rootOpts2(this));
763
+ let entries;
764
+ try {
765
+ entries = parseQuestionnaire(options.file);
766
+ } catch (e) {
767
+ dieWith(
768
+ new Error(
769
+ `Failed to parse ${options.file}: ${e.message}`
770
+ )
771
+ );
772
+ }
773
+ const filtered = options.level ? entries.filter(
774
+ (e) => (e.level ?? "").toLowerCase().includes(options.level.toLowerCase())
775
+ ) : entries;
776
+ if (filtered.length === 0) {
777
+ dieWith(
778
+ new Error(
779
+ options.level ? `No respondents matched level "${options.level}".` : "No respondents found in questionnaire."
780
+ )
781
+ );
782
+ }
783
+ if (options.dryRun) {
784
+ printTable(
785
+ filtered.map((e) => ({
786
+ name: e.name,
787
+ email: e.email,
788
+ dupr: typeof e.duprRating === "number" ? e.duprRating.toFixed(2) : "\u2014",
789
+ level: e.level ?? "\u2014"
790
+ })),
791
+ [
792
+ { key: "name", label: "NAME" },
793
+ { key: "email", label: "EMAIL" },
794
+ { key: "dupr", label: "DUPR" },
795
+ { key: "level", label: "LEVEL" }
796
+ ]
797
+ );
798
+ console.log(`
799
+ ${filtered.length} player(s) would be upserted.`);
800
+ return;
801
+ }
802
+ try {
803
+ const res = await client.post("/api/players", {
804
+ players: filtered.map((e) => ({
805
+ email: e.email,
806
+ name: e.name,
807
+ duprRating: e.duprRating,
808
+ duprId: e.duprId,
809
+ selfRating: e.selfRating,
810
+ instagram: e.instagram,
811
+ slackJoined: e.slackJoined
812
+ }))
813
+ });
814
+ const created = res.results.filter(
815
+ (r) => r.status === "created"
816
+ ).length;
817
+ const updated = res.results.filter(
818
+ (r) => r.status === "updated"
819
+ ).length;
820
+ const linked = res.results.reduce(
821
+ (acc, r) => acc + r.linkedRosters,
822
+ 0
823
+ );
824
+ printTable(
825
+ res.results.map((r) => ({
826
+ name: r.name,
827
+ email: r.email,
828
+ status: r.status,
829
+ linked: r.linkedRosters
830
+ })),
831
+ [
832
+ { key: "name", label: "NAME" },
833
+ { key: "email", label: "EMAIL" },
834
+ { key: "status", label: "STATUS" },
835
+ { key: "linked", label: "LINKED ROSTERS" }
836
+ ]
837
+ );
838
+ console.log(
839
+ `
840
+ Done. created=${created} updated=${updated} roster-links=${linked}`
841
+ );
842
+ if (res.errors.length > 0) {
843
+ console.log(`Errors:`);
844
+ for (const e of res.errors)
845
+ console.log(` row ${e.index}: ${e.message}`);
846
+ }
847
+ } catch (e) {
848
+ dieWith(e);
849
+ }
850
+ });
851
+ }
852
+
642
853
  // src/index.ts
643
- var program = new Command2();
854
+ var program = new Command3();
644
855
  program.name("nycp").description("CLI for managing NYC Pickleball leagues via the deployed API").version("0.1.0").option(
645
856
  "--url <url>",
646
857
  "API base URL (default: $NYCP_API_URL or https://nycpickleball.vercel.app)"
647
858
  ).option("--token <token>", "API token (default: $NYCP_API_KEY)").option("--json", "Force JSON output where the default would be a table");
648
859
  registerLeagueCommands(program);
860
+ registerPlayerCommands(program);
649
861
  program.parseAsync(process.argv).catch((err) => {
650
862
  process.stderr.write(
651
863
  `Unexpected error: ${err instanceof Error ? err.message : String(err)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nycpickleball/cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for managing NYC Pickleball leagues via the deployed API.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {