@nycpickleball/cli 1.0.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 +298 -0
  2. package/package.json +38 -0
package/dist/index.js ADDED
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command2 } from "commander";
5
+
6
+ // src/commands/league.ts
7
+ import { readFileSync } from "fs";
8
+ import { Option } from "commander";
9
+
10
+ // src/api.ts
11
+ var ApiError = class extends Error {
12
+ status;
13
+ body;
14
+ constructor(status, body, message) {
15
+ super(message);
16
+ this.status = status;
17
+ this.body = body;
18
+ }
19
+ };
20
+ function createClient(opts) {
21
+ const base = opts.baseUrl.replace(/\/+$/, "");
22
+ async function request(method, path, body, opts2 = { auth: true }) {
23
+ const url = `${base}${path}`;
24
+ const headers = {
25
+ Accept: "application/json"
26
+ };
27
+ if (body !== void 0) headers["Content-Type"] = "application/json";
28
+ if (opts2.auth !== false) {
29
+ headers["Authorization"] = `Bearer ${getToken()}`;
30
+ }
31
+ const res = await fetch(url, {
32
+ method,
33
+ headers,
34
+ body: body !== void 0 ? JSON.stringify(body) : void 0
35
+ });
36
+ let parsed = null;
37
+ const text = await res.text();
38
+ if (text) {
39
+ try {
40
+ parsed = JSON.parse(text);
41
+ } catch {
42
+ parsed = text;
43
+ }
44
+ }
45
+ if (!res.ok) {
46
+ const msg = parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : `HTTP ${res.status}`;
47
+ throw new ApiError(res.status, parsed, msg);
48
+ }
49
+ return parsed;
50
+ }
51
+ function getToken() {
52
+ if (!opts.token) {
53
+ throw new Error(
54
+ "No API token provided. Set NYCP_API_KEY env var or pass --token."
55
+ );
56
+ }
57
+ return opts.token;
58
+ }
59
+ return {
60
+ get: (path, options) => request("GET", path, void 0, options ?? { auth: false }),
61
+ post: (path, body) => request("POST", path, body),
62
+ put: (path, body) => request("PUT", path, body),
63
+ delete: (path) => request("DELETE", path)
64
+ };
65
+ }
66
+
67
+ // src/config.ts
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();
72
+ const client = createClient({ baseUrl, token });
73
+ return { baseUrl, token, client };
74
+ }
75
+
76
+ // src/output.ts
77
+ function printJson(value) {
78
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
79
+ }
80
+ function printTable(rows, columns) {
81
+ if (rows.length === 0) {
82
+ process.stdout.write("(no results)\n");
83
+ return;
84
+ }
85
+ const widths = columns.map(
86
+ (c) => Math.max(
87
+ c.label.length,
88
+ ...rows.map((r) => String(r[c.key] ?? "").length)
89
+ )
90
+ );
91
+ const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ") + "\n";
92
+ process.stdout.write(line(columns.map((c) => c.label)));
93
+ process.stdout.write(line(widths.map((w) => "-".repeat(w))));
94
+ for (const r of rows) {
95
+ process.stdout.write(
96
+ line(columns.map((c) => String(r[c.key] ?? "")))
97
+ );
98
+ }
99
+ }
100
+ function dieWith(error) {
101
+ if (error instanceof Error) {
102
+ process.stderr.write(`Error: ${error.message}
103
+ `);
104
+ } else {
105
+ process.stderr.write(`Error: ${String(error)}
106
+ `);
107
+ }
108
+ process.exit(1);
109
+ }
110
+
111
+ // src/commands/league.ts
112
+ var LEAGUE_TYPES = ["TRIPLES", "LEVEL_TWO", "LEVEL_THREE", "LEVEL_FOUR"];
113
+ var DAYS = [
114
+ "MONDAY",
115
+ "TUESDAY",
116
+ "WEDNESDAY",
117
+ "THURSDAY",
118
+ "FRIDAY",
119
+ "SATURDAY",
120
+ "SUNDAY"
121
+ ];
122
+ var STATUSES = ["UPCOMING", "ACTIVE", "ARCHIVED"];
123
+ function rootOpts(cmd) {
124
+ let c = cmd;
125
+ while (c && c.parent) c = c.parent;
126
+ return c?.opts() ?? {};
127
+ }
128
+ function intOpt(value) {
129
+ const n = parseInt(value, 10);
130
+ if (!Number.isFinite(n)) throw new Error(`Not an integer: ${value}`);
131
+ return n;
132
+ }
133
+ function registerLeagueCommands(program2) {
134
+ const league = program2.command("league").description("Manage leagues");
135
+ league.command("list").description("List all leagues").action(async function() {
136
+ const opts = rootOpts(this);
137
+ const { client } = resolveConfig(opts);
138
+ try {
139
+ const res = await client.get("/api/league");
140
+ if (opts.json) {
141
+ printJson(res.data);
142
+ } else {
143
+ printTable(
144
+ res.data.map((l) => ({
145
+ slug: l.slug,
146
+ name: l.name,
147
+ type: l.leagueType,
148
+ day: l.dayOfWeek,
149
+ status: l.status.toLowerCase(),
150
+ courts: l._count?.courts ?? 0
151
+ })),
152
+ [
153
+ { key: "slug", label: "SLUG" },
154
+ { key: "name", label: "NAME" },
155
+ { key: "type", label: "TYPE" },
156
+ { key: "day", label: "DAY" },
157
+ { key: "status", label: "STATUS" },
158
+ { key: "courts", label: "COURTS" }
159
+ ]
160
+ );
161
+ }
162
+ } catch (e) {
163
+ dieWith(e);
164
+ }
165
+ });
166
+ league.command("get <slug>").description("Get a single league including its roster").action(async function(slug) {
167
+ const { client } = resolveConfig(rootOpts(this));
168
+ try {
169
+ const data = await client.get(`/api/league/${encodeURIComponent(slug)}`);
170
+ printJson(data);
171
+ } catch (e) {
172
+ dieWith(e);
173
+ }
174
+ });
175
+ league.command("create").description("Create a new league").requiredOption("--name <name>", "Display name").option("--slug <slug>", "URL slug (derived from name if omitted)").addOption(
176
+ new Option("--type <type>", "League type").choices(LEAGUE_TYPES).makeOptionMandatory()
177
+ ).addOption(
178
+ new Option("--day <day>", "Day of week").choices(DAYS).makeOptionMandatory()
179
+ ).requiredOption("--start <date>", "Start date (YYYY-MM-DD)").option("--time-range <range>", "Time range, e.g. '8 - 10 PM'").option("--location <name>", "Location").option("--total-weeks <n>", "Total weeks", intOpt).option("--weeks-played <n>", "Weeks played", intOpt).addOption(
180
+ new Option("--status <status>", "Status").choices(
181
+ STATUSES
182
+ )
183
+ ).option("--slack-channel-name <name>", "Slack channel name").option("--slack-channel-url <url>", "Slack channel URL").action(async function(options) {
184
+ const { client } = resolveConfig(rootOpts(this));
185
+ const body = {
186
+ name: options.name,
187
+ leagueType: options.type,
188
+ dayOfWeek: options.day,
189
+ startTime: options.start
190
+ };
191
+ if (options.slug) body.slug = options.slug;
192
+ if (options.location) body.location = options.location;
193
+ if (options.timeRange) body.timeRange = options.timeRange;
194
+ if (typeof options.totalWeeks === "number")
195
+ body.totalWeeks = options.totalWeeks;
196
+ if (typeof options.weeksPlayed === "number")
197
+ body.weeksPlayed = options.weeksPlayed;
198
+ if (options.status) body.status = options.status;
199
+ if (options.slackChannelName)
200
+ body.slackChannelName = options.slackChannelName;
201
+ if (options.slackChannelUrl)
202
+ body.slackChannelUrl = options.slackChannelUrl;
203
+ try {
204
+ const created = await client.post("/api/league", body);
205
+ printJson(created);
206
+ } catch (e) {
207
+ dieWith(e);
208
+ }
209
+ });
210
+ league.command("update <slug>").description("Update fields on an existing league").option("--name <name>").option("--slug <newSlug>", "Rename the slug").addOption(
211
+ new Option("--type <type>").choices(LEAGUE_TYPES)
212
+ ).addOption(new Option("--day <day>").choices(DAYS)).option("--start <date>").option("--time-range <range>").option("--location <name>").option("--total-weeks <n>", "Total weeks", intOpt).option("--weeks-played <n>", "Weeks played", intOpt).addOption(
213
+ new Option("--status <status>").choices(
214
+ STATUSES
215
+ )
216
+ ).option("--slack-channel-name <name>").option("--slack-channel-url <url>").action(async function(slug, options) {
217
+ const { client } = resolveConfig(rootOpts(this));
218
+ const body = {};
219
+ if (options.name !== void 0) body.name = options.name;
220
+ if (options.slug !== void 0) body.slug = options.slug;
221
+ if (options.type !== void 0) body.leagueType = options.type;
222
+ if (options.day !== void 0) body.dayOfWeek = options.day;
223
+ if (options.start !== void 0) body.startTime = options.start;
224
+ if (options.timeRange !== void 0) body.timeRange = options.timeRange;
225
+ if (options.location !== void 0) body.location = options.location;
226
+ if (typeof options.totalWeeks === "number")
227
+ body.totalWeeks = options.totalWeeks;
228
+ if (typeof options.weeksPlayed === "number")
229
+ body.weeksPlayed = options.weeksPlayed;
230
+ if (options.status !== void 0) body.status = options.status;
231
+ if (options.slackChannelName !== void 0)
232
+ body.slackChannelName = options.slackChannelName;
233
+ if (options.slackChannelUrl !== void 0)
234
+ body.slackChannelUrl = options.slackChannelUrl;
235
+ try {
236
+ const updated = await client.put(
237
+ `/api/league/${encodeURIComponent(slug)}`,
238
+ body
239
+ );
240
+ printJson(updated);
241
+ } catch (e) {
242
+ dieWith(e);
243
+ }
244
+ });
245
+ league.command("delete <slug>").description("Delete a league (also deletes its courts and players)").action(async function(slug) {
246
+ const { client } = resolveConfig(rootOpts(this));
247
+ try {
248
+ const res = await client.delete(
249
+ `/api/league/${encodeURIComponent(slug)}`
250
+ );
251
+ printJson(res);
252
+ } catch (e) {
253
+ dieWith(e);
254
+ }
255
+ });
256
+ const roster = league.command("roster").description("Manage a league's roster");
257
+ roster.command("import <slug>").description("Replace a league's roster from a JSON file").requiredOption(
258
+ "--file <path>",
259
+ "Path to JSON with shape { courts: [{ name, players: [...] }] }"
260
+ ).action(async function(slug, options) {
261
+ const { client } = resolveConfig(rootOpts(this));
262
+ let parsed;
263
+ try {
264
+ const text = readFileSync(options.file, "utf8");
265
+ parsed = JSON.parse(text);
266
+ } catch (e) {
267
+ dieWith(
268
+ new Error(
269
+ `Could not read or parse ${options.file}: ${e.message}`
270
+ )
271
+ );
272
+ }
273
+ try {
274
+ const res = await client.post(
275
+ `/api/league/${encodeURIComponent(slug)}/roster`,
276
+ parsed
277
+ );
278
+ printJson(res);
279
+ } catch (e) {
280
+ dieWith(e);
281
+ }
282
+ });
283
+ }
284
+
285
+ // src/index.ts
286
+ var program = new Command2();
287
+ program.name("nycp").description("CLI for managing NYC Pickleball leagues via the deployed API").version("0.1.0").option(
288
+ "--url <url>",
289
+ "API base URL (default: $NYCP_API_URL or https://nycpickleball.vercel.app)"
290
+ ).option("--token <token>", "API token (default: $NYCP_API_KEY)").option("--json", "Force JSON output where the default would be a table");
291
+ registerLeagueCommands(program);
292
+ program.parseAsync(process.argv).catch((err) => {
293
+ process.stderr.write(
294
+ `Unexpected error: ${err instanceof Error ? err.message : String(err)}
295
+ `
296
+ );
297
+ process.exit(1);
298
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@nycpickleball/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for managing NYC Pickleball leagues via the deployed API.",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "bin": {
11
+ "nycp": "dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsx src/index.ts",
20
+ "compile": "tsc --noEmit",
21
+ "prepublishOnly": "pnpm build"
22
+ },
23
+ "dependencies": {
24
+ "commander": "^12.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@semantic-release/changelog": "^6.0.3",
28
+ "@semantic-release/git": "^10.0.1",
29
+ "@types/node": "^20",
30
+ "semantic-release": "^24.2.1",
31
+ "tsup": "^8.3.0",
32
+ "tsx": "^4.22.4",
33
+ "typescript": "^5"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }