@countrystatecity/cli 0.1.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 ADDED
@@ -0,0 +1,1578 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import chalk3 from "chalk";
8
+ import readline from "readline";
9
+ import { Writable } from "stream";
10
+
11
+ // src/lib/config.ts
12
+ import Conf from "conf";
13
+ var config = new Conf({
14
+ projectName: "csc",
15
+ schema: {
16
+ apiKey: { type: "string", default: "" },
17
+ apiBase: { type: "string", default: "https://api.countrystatecity.in/v1" }
18
+ }
19
+ });
20
+ function getApiKey() {
21
+ const key = config.get("apiKey");
22
+ return key || void 0;
23
+ }
24
+ function setApiKey(key) {
25
+ config.set("apiKey", key);
26
+ }
27
+ function clearApiKey() {
28
+ config.delete("apiKey");
29
+ }
30
+ function getApiBase() {
31
+ return config.get("apiBase");
32
+ }
33
+
34
+ // src/lib/api.ts
35
+ import axios, { AxiosError } from "axios";
36
+ import chalk from "chalk";
37
+ function extractUsage(headers) {
38
+ const dailyUsed = headers["x-csc-daily-used"];
39
+ const dailyLimit = headers["x-csc-daily-limit"];
40
+ const monthlyUsed = headers["x-csc-monthly-used"];
41
+ const monthlyLimit = headers["x-csc-monthly-limit"];
42
+ if (!dailyUsed || !dailyLimit || !monthlyUsed || !monthlyLimit) {
43
+ return null;
44
+ }
45
+ return {
46
+ dailyUsed: parseInt(dailyUsed, 10),
47
+ dailyLimit: parseInt(dailyLimit, 10),
48
+ monthlyUsed: parseInt(monthlyUsed, 10),
49
+ monthlyLimit: parseInt(monthlyLimit, 10)
50
+ };
51
+ }
52
+ async function get(path) {
53
+ const apiKey = getApiKey();
54
+ if (!apiKey) {
55
+ console.error(chalk.red("Not authenticated."));
56
+ console.error(chalk.dim("Run `csc auth login` to set your API key."));
57
+ process.exit(1);
58
+ }
59
+ const baseUrl = getApiBase();
60
+ try {
61
+ const response = await axios.get(`${baseUrl}${path}`, {
62
+ headers: {
63
+ "X-CSCAPI-KEY": apiKey,
64
+ "User-Agent": "@countrystatecity/cli/0.1.1"
65
+ }
66
+ });
67
+ const usage = extractUsage(response.headers);
68
+ return { data: response.data, usage };
69
+ } catch (error) {
70
+ if (error instanceof AxiosError) {
71
+ const status = error.response?.status;
72
+ if (status === 401) {
73
+ console.error(chalk.red("Invalid or missing API key."));
74
+ console.error(chalk.dim("Run `csc auth login` to set your key."));
75
+ process.exit(1);
76
+ }
77
+ if (status === 429) {
78
+ console.error(chalk.red("Daily limit reached."));
79
+ console.error(chalk.yellow("Run `csc upgrade` to increase your limits."));
80
+ process.exit(1);
81
+ }
82
+ if (status === 404) {
83
+ console.error(chalk.red("Not found."));
84
+ process.exit(1);
85
+ }
86
+ console.error(chalk.red(`API error: ${error.message}`));
87
+ process.exit(1);
88
+ }
89
+ console.error(chalk.red("Cannot reach API. Check your internet connection."));
90
+ process.exit(1);
91
+ }
92
+ }
93
+ async function validateKey(apiKey) {
94
+ const baseUrl = getApiBase();
95
+ try {
96
+ const response = await axios.get(`${baseUrl}/countries/IN`, {
97
+ headers: {
98
+ "X-CSCAPI-KEY": apiKey,
99
+ "User-Agent": "@countrystatecity/cli/0.1.1"
100
+ }
101
+ });
102
+ const usage = extractUsage(response.headers);
103
+ return { valid: true, usage };
104
+ } catch {
105
+ return { valid: false, usage: null };
106
+ }
107
+ }
108
+
109
+ // src/lib/usage-footer.ts
110
+ import chalk2 from "chalk";
111
+ function getTierName(dailyLimit) {
112
+ if (dailyLimit <= 100) return "Community";
113
+ if (dailyLimit <= 300) return "Starter";
114
+ if (dailyLimit <= 1e3) return "Supporter";
115
+ if (dailyLimit <= 3300) return "Professional";
116
+ if (dailyLimit <= 25e3) return "Business";
117
+ if (dailyLimit <= 5e4) return "Legacy";
118
+ return "Custom";
119
+ }
120
+ function formatNumber(n) {
121
+ return n.toLocaleString("en-US");
122
+ }
123
+ function timeUntilDailyReset() {
124
+ const now = Date.now();
125
+ const resetAt = /* @__PURE__ */ new Date();
126
+ resetAt.setUTCHours(24, 0, 0, 0);
127
+ const ms = resetAt.getTime() - now;
128
+ const hours = Math.floor(ms / (1e3 * 60 * 60));
129
+ const minutes = Math.floor(ms % (1e3 * 60 * 60) / (1e3 * 60));
130
+ return `${hours}h ${minutes}m`;
131
+ }
132
+ function printUsageFooter(usage, flags) {
133
+ if (flags?.noFooter || flags?.json) return;
134
+ if (!usage) return;
135
+ const { dailyUsed, dailyLimit, monthlyUsed, monthlyLimit } = usage;
136
+ if (dailyLimit === 0 || monthlyLimit === 0) return;
137
+ const dailyPercent = dailyUsed / dailyLimit * 100;
138
+ const tier = getTierName(dailyLimit);
139
+ process.stderr.write("\n");
140
+ if (dailyPercent >= 100) {
141
+ process.stderr.write(
142
+ chalk2.red(
143
+ `Daily limit reached (${formatNumber(dailyUsed)}/${formatNumber(dailyLimit)}). Resets in ${timeUntilDailyReset()}.`
144
+ ) + "\n"
145
+ );
146
+ const nextTier = dailyLimit <= 300 ? "Supporter ($9/mo)" : "a higher plan";
147
+ process.stderr.write(chalk2.red(`Upgrade to ${nextTier} for more requests/day.`) + "\n");
148
+ process.stderr.write(
149
+ chalk2.red("Run `csc upgrade` or visit https://app.countrystatecity.in/pricing") + "\n"
150
+ );
151
+ } else if (dailyPercent >= 80) {
152
+ process.stderr.write(
153
+ chalk2.yellow(
154
+ `Warning: ${formatNumber(dailyUsed)}/${formatNumber(dailyLimit)} daily requests used (${Math.round(dailyPercent)}%). Upgrade for more at $9/mo.`
155
+ ) + "\n"
156
+ );
157
+ process.stderr.write(chalk2.yellow("Run `csc upgrade` to view plans.") + "\n");
158
+ } else {
159
+ process.stderr.write(
160
+ chalk2.dim(
161
+ `Usage: ${formatNumber(dailyUsed)}/${formatNumber(dailyLimit)} today | ${formatNumber(monthlyUsed)}/${formatNumber(monthlyLimit)} this month (${tier})`
162
+ ) + "\n"
163
+ );
164
+ }
165
+ }
166
+ function progressBar(used, limit) {
167
+ if (limit === 0) return "[--------------------]";
168
+ const ratio = Math.min(used / limit, 1);
169
+ const filled = Math.round(ratio * 20);
170
+ const empty = 20 - filled;
171
+ const percent = Math.round(ratio * 100);
172
+ let colorFn;
173
+ if (percent >= 80) colorFn = chalk2.red;
174
+ else if (percent >= 60) colorFn = chalk2.yellow;
175
+ else colorFn = chalk2.green;
176
+ return colorFn(`[${"=".repeat(filled)}${"-".repeat(empty)}]`);
177
+ }
178
+
179
+ // src/lib/output.ts
180
+ import search from "@inquirer/search";
181
+ function stderr(message) {
182
+ process.stderr.write(message + "\n");
183
+ }
184
+ async function createSpinner(text, flags) {
185
+ if (flags.quiet || flags.json) {
186
+ return buildNoopSpinner(text);
187
+ }
188
+ const ora = (await import("ora")).default;
189
+ const spinner = ora({ text, stream: process.stderr });
190
+ spinner.start();
191
+ return spinner;
192
+ }
193
+ function isTTY() {
194
+ return !!process.stdin.isTTY;
195
+ }
196
+ async function promptCountry(countries) {
197
+ return search({
198
+ message: "Select a country",
199
+ source: (input) => {
200
+ const query = (input ?? "").toLowerCase();
201
+ return Promise.resolve(
202
+ countries.filter(
203
+ (c) => c.name.toLowerCase().includes(query) || c.iso2.toLowerCase().includes(query)
204
+ ).map((c) => ({
205
+ name: c.emoji ? `${c.emoji} ${c.name}` : c.name,
206
+ value: c.iso2
207
+ }))
208
+ );
209
+ }
210
+ });
211
+ }
212
+ async function promptState(states) {
213
+ return search({
214
+ message: "Select a state",
215
+ source: (input) => {
216
+ const query = (input ?? "").toLowerCase();
217
+ return Promise.resolve(
218
+ states.filter(
219
+ (s) => s.name.toLowerCase().includes(query) || s.iso2.toLowerCase().includes(query)
220
+ ).map((s) => ({ name: s.name, value: s.iso2 }))
221
+ );
222
+ }
223
+ });
224
+ }
225
+ function buildNoopSpinner(initialText) {
226
+ const noop = {
227
+ text: initialText,
228
+ start(_text) {
229
+ return this;
230
+ },
231
+ stop() {
232
+ },
233
+ succeed(_text) {
234
+ },
235
+ fail(_text) {
236
+ }
237
+ };
238
+ return noop;
239
+ }
240
+
241
+ // src/commands/auth.ts
242
+ async function promptForKey() {
243
+ return new Promise((resolve) => {
244
+ let muted = false;
245
+ let settled = false;
246
+ const mutableStdout = new Writable({
247
+ write(chunk, encoding, callback) {
248
+ if (!muted) {
249
+ process.stdout.write(chunk, encoding);
250
+ }
251
+ callback();
252
+ }
253
+ });
254
+ const rl = readline.createInterface({
255
+ input: process.stdin,
256
+ output: mutableStdout,
257
+ terminal: true
258
+ });
259
+ const finish = (answer) => {
260
+ if (settled) return;
261
+ settled = true;
262
+ rl.close();
263
+ console.log();
264
+ resolve(answer.trim());
265
+ };
266
+ rl.question(
267
+ `Enter your API key (get one at ${chalk3.cyan("https://app.countrystatecity.in")}): `,
268
+ (answer) => finish(answer)
269
+ );
270
+ rl.on("close", () => {
271
+ if (!settled) {
272
+ settled = true;
273
+ console.log();
274
+ resolve("");
275
+ }
276
+ });
277
+ muted = true;
278
+ });
279
+ }
280
+ function registerAuthCommands(program2) {
281
+ const auth = program2.command("auth").description("Manage API authentication");
282
+ auth.command("login").description("Authenticate with your API key").option("--key <apiKey>", "Provide API key directly").action(async (options, cmd) => {
283
+ const globalOpts = cmd.optsWithGlobals();
284
+ const flags = {
285
+ json: globalOpts.json ?? false,
286
+ quiet: globalOpts.quiet ?? false,
287
+ noFooter: globalOpts.footer === false
288
+ };
289
+ if (!options.key && !isTTY()) {
290
+ process.stderr.write(chalk3.red("API key required in non-interactive mode.") + "\n");
291
+ process.stderr.write(
292
+ chalk3.dim("Use `csc auth login --key <API_KEY>` to provide it directly.") + "\n"
293
+ );
294
+ process.exit(1);
295
+ }
296
+ const key = options.key || await promptForKey();
297
+ if (!key) {
298
+ process.stderr.write(chalk3.red("No API key provided.") + "\n");
299
+ process.exit(1);
300
+ }
301
+ const spinner = await createSpinner("Validating API key...", flags);
302
+ const result = await validateKey(key);
303
+ if (!result.valid) {
304
+ spinner.fail("Invalid API key.");
305
+ process.stderr.write(chalk3.dim("Check your key at https://app.countrystatecity.in") + "\n");
306
+ process.exit(1);
307
+ }
308
+ setApiKey(key);
309
+ spinner.succeed("API key saved successfully.");
310
+ if (result.usage) {
311
+ const tier = getTierName(result.usage.dailyLimit);
312
+ process.stderr.write(
313
+ chalk3.dim(
314
+ `Tier: ${tier} | ${formatNumber(result.usage.dailyLimit)}/day | ${formatNumber(result.usage.monthlyLimit)}/month`
315
+ ) + "\n"
316
+ );
317
+ }
318
+ });
319
+ auth.command("status").description("Check authentication status").action(async (_options, cmd) => {
320
+ const globalOpts = cmd.optsWithGlobals();
321
+ const flags = {
322
+ json: globalOpts.json ?? false,
323
+ quiet: globalOpts.quiet ?? false,
324
+ noFooter: globalOpts.footer === false
325
+ };
326
+ const key = getApiKey();
327
+ if (!key) {
328
+ if (flags.json) {
329
+ process.stdout.write(JSON.stringify({ authenticated: false }) + "\n");
330
+ } else {
331
+ process.stderr.write(chalk3.yellow("Not logged in.") + "\n");
332
+ process.stderr.write(chalk3.dim("Run `csc auth login` to set your API key.") + "\n");
333
+ }
334
+ return;
335
+ }
336
+ const spinner = await createSpinner("Checking status...", flags);
337
+ const result = await validateKey(key);
338
+ if (!result.valid) {
339
+ spinner.fail("Stored API key is invalid.");
340
+ process.stderr.write(chalk3.dim("Run `csc auth login` to set a new key.") + "\n");
341
+ return;
342
+ }
343
+ spinner.stop();
344
+ if (flags.json) {
345
+ const masked2 = "****..." + key.slice(-4);
346
+ const output = { authenticated: true, key: masked2 };
347
+ if (result.usage) {
348
+ const tier = getTierName(result.usage.dailyLimit);
349
+ output.tier = tier;
350
+ output.daily = { used: result.usage.dailyUsed, limit: result.usage.dailyLimit };
351
+ output.monthly = { used: result.usage.monthlyUsed, limit: result.usage.monthlyLimit };
352
+ }
353
+ process.stdout.write(JSON.stringify(output) + "\n");
354
+ return;
355
+ }
356
+ const masked = "****..." + key.slice(-4);
357
+ console.log(chalk3.green("Authenticated"));
358
+ console.log(`${chalk3.bold("Key:".padEnd(10))}${masked}`);
359
+ if (result.usage) {
360
+ const tier = getTierName(result.usage.dailyLimit);
361
+ console.log(`${chalk3.bold("Tier:".padEnd(10))}${tier}`);
362
+ console.log(
363
+ `${chalk3.bold("Daily:".padEnd(10))}${formatNumber(result.usage.dailyUsed)} / ${formatNumber(result.usage.dailyLimit)}`
364
+ );
365
+ console.log(
366
+ `${chalk3.bold("Monthly:".padEnd(10))}${formatNumber(result.usage.monthlyUsed)} / ${formatNumber(result.usage.monthlyLimit)}`
367
+ );
368
+ }
369
+ });
370
+ auth.command("logout").description("Remove stored API key").action((_options, cmd) => {
371
+ const globalOpts = cmd.optsWithGlobals();
372
+ const flags = {
373
+ json: globalOpts.json ?? false,
374
+ quiet: globalOpts.quiet ?? false,
375
+ noFooter: globalOpts.footer === false
376
+ };
377
+ clearApiKey();
378
+ if (flags.json) {
379
+ process.stdout.write(JSON.stringify({ success: true }) + "\n");
380
+ } else {
381
+ console.log(chalk3.green("API key removed."));
382
+ }
383
+ });
384
+ }
385
+
386
+ // src/commands/search.ts
387
+ import chalk5 from "chalk";
388
+
389
+ // src/lib/display.ts
390
+ import Table from "cli-table3";
391
+ import chalk4 from "chalk";
392
+ function printTable(headers, rows) {
393
+ const table = new Table({
394
+ head: headers.map((h) => chalk4.cyan(h)),
395
+ style: { head: [], border: [] }
396
+ });
397
+ for (const row of rows) {
398
+ table.push(row);
399
+ }
400
+ console.log(table.toString());
401
+ }
402
+ function printJson(data) {
403
+ const json = JSON.stringify(data, null, 2);
404
+ const highlighted = json.replace(
405
+ /"([^"]+)"\s*:|:\s*"((?:[^"\\]|\\.)*)"|:\s*(-?\d+(?:\.\d+)?)/g,
406
+ (match, key, strVal, numVal) => {
407
+ if (key !== void 0) return `${chalk4.cyan(`"${key}"`)}:`;
408
+ if (strVal !== void 0) return `: ${chalk4.green(`"${strVal}"`)}`;
409
+ if (numVal !== void 0) return `: ${chalk4.yellow(numVal)}`;
410
+ return match;
411
+ }
412
+ );
413
+ console.log(highlighted);
414
+ }
415
+ function printDetail(label, value) {
416
+ console.log(`${chalk4.bold(label.padEnd(14))}${value}`);
417
+ }
418
+
419
+ // src/commands/search.ts
420
+ function registerSearchCommands(program2) {
421
+ const search3 = program2.command("search").description("Search countries, states, and cities");
422
+ search3.command("countries").description("List all countries").option("--filter <text>", "Filter by name").action(async (options, cmd) => {
423
+ const globalOpts = cmd.optsWithGlobals();
424
+ const flags = {
425
+ json: globalOpts.json ?? false,
426
+ quiet: globalOpts.quiet ?? false,
427
+ noFooter: globalOpts.footer === false
428
+ };
429
+ const spinner = await createSpinner("Fetching countries...", flags);
430
+ const { data, usage } = await get("/countries");
431
+ spinner.stop();
432
+ let countries = data;
433
+ if (options.filter) {
434
+ const term = options.filter.toLowerCase();
435
+ countries = countries.filter((c) => c.name.toLowerCase().includes(term));
436
+ }
437
+ if (flags.json) {
438
+ printJson(countries);
439
+ } else {
440
+ const rows = countries.map((c) => [
441
+ c.iso2,
442
+ c.iso3,
443
+ c.name,
444
+ c.capital || "",
445
+ c.phonecode ? `+${c.phonecode.replace(/^\+/, "")}` : "",
446
+ c.currency || ""
447
+ ]);
448
+ printTable(["ISO2", "ISO3", "Name", "Capital", "Phone", "Currency"], rows);
449
+ }
450
+ printUsageFooter(usage, flags);
451
+ });
452
+ search3.command("states").description("List states for a country").option("-c, --country <iso2>", "Country ISO2 code").option("--filter <text>", "Filter by name").action(async (options, cmd) => {
453
+ const globalOpts = cmd.optsWithGlobals();
454
+ const flags = {
455
+ json: globalOpts.json ?? false,
456
+ quiet: globalOpts.quiet ?? false,
457
+ noFooter: globalOpts.footer === false
458
+ };
459
+ let code = options.country?.toUpperCase();
460
+ if (!code) {
461
+ if (isTTY()) {
462
+ const countrySpinner = await createSpinner("Loading countries...", flags);
463
+ const { data: allCountries } = await get("/countries");
464
+ countrySpinner.stop();
465
+ code = await promptCountry(allCountries);
466
+ } else {
467
+ process.stderr.write(chalk5.red("Country code required. Use --country IN\n"));
468
+ process.exit(1);
469
+ return;
470
+ }
471
+ }
472
+ const spinner = await createSpinner(`Fetching states for ${code}...`, flags);
473
+ const { data, usage } = await get(`/countries/${code}/states`);
474
+ spinner.stop();
475
+ let states = data;
476
+ if (options.filter) {
477
+ const term = options.filter.toLowerCase();
478
+ states = states.filter((s) => s.name.toLowerCase().includes(term));
479
+ }
480
+ if (flags.json) {
481
+ printJson(states);
482
+ } else {
483
+ const rows = states.map((s) => [
484
+ String(s.id),
485
+ s.name,
486
+ s.iso2 || "",
487
+ s.type || ""
488
+ ]);
489
+ printTable(["ID", "Name", "ISO2", "Type"], rows);
490
+ }
491
+ printUsageFooter(usage, flags);
492
+ });
493
+ search3.command("cities").description("List cities for a state").option("-c, --country <iso2>", "Country ISO2 code").option("-s, --state <iso2>", "State ISO2 code").option("--filter <text>", "Filter by name").action(
494
+ async (options, cmd) => {
495
+ const globalOpts = cmd.optsWithGlobals();
496
+ const flags = {
497
+ json: globalOpts.json ?? false,
498
+ quiet: globalOpts.quiet ?? false,
499
+ noFooter: globalOpts.footer === false
500
+ };
501
+ let countryCode = options.country?.toUpperCase();
502
+ if (!countryCode) {
503
+ if (isTTY()) {
504
+ const countrySpinner = await createSpinner("Loading countries...", flags);
505
+ const { data: allCountries } = await get("/countries");
506
+ countrySpinner.stop();
507
+ countryCode = await promptCountry(allCountries);
508
+ } else {
509
+ process.stderr.write(chalk5.red("Country code required. Use --country IN\n"));
510
+ process.exit(1);
511
+ return;
512
+ }
513
+ }
514
+ let stateCode = options.state?.toUpperCase();
515
+ if (!stateCode) {
516
+ if (isTTY()) {
517
+ const stateSpinner = await createSpinner(`Loading states for ${countryCode}...`, flags);
518
+ const { data: allStates } = await get(`/countries/${countryCode}/states`);
519
+ stateSpinner.stop();
520
+ stateCode = await promptState(allStates);
521
+ } else {
522
+ process.stderr.write(chalk5.red("State code required. Use --state MH\n"));
523
+ process.exit(1);
524
+ return;
525
+ }
526
+ }
527
+ const spinner = await createSpinner(
528
+ `Fetching cities for ${countryCode}/${stateCode}...`,
529
+ flags
530
+ );
531
+ const { data, usage } = await get(
532
+ `/countries/${countryCode}/states/${stateCode}/cities`
533
+ );
534
+ spinner.stop();
535
+ let cities = data;
536
+ if (options.filter) {
537
+ const term = options.filter.toLowerCase();
538
+ cities = cities.filter((c) => c.name.toLowerCase().includes(term));
539
+ }
540
+ if (flags.json) {
541
+ printJson(cities);
542
+ } else {
543
+ const rows = cities.map((c) => [String(c.id), c.name]);
544
+ printTable(["ID", "Name"], rows);
545
+ }
546
+ printUsageFooter(usage, flags);
547
+ }
548
+ );
549
+ search3.argument("[query]", "Search term to match country names").action(async (query, options, cmd) => {
550
+ if (!query) return;
551
+ const globalOpts = cmd.optsWithGlobals();
552
+ const flags = {
553
+ json: globalOpts.json ?? false,
554
+ quiet: globalOpts.quiet ?? false,
555
+ noFooter: globalOpts.footer === false
556
+ };
557
+ const spinner = await createSpinner("Searching...", flags);
558
+ const { data, usage } = await get("/countries");
559
+ spinner.stop();
560
+ const term = query.toLowerCase();
561
+ const matches = data.filter((c) => c.name.toLowerCase().includes(term));
562
+ if (flags.json) {
563
+ printJson(matches);
564
+ } else if (matches.length === 0) {
565
+ console.log(chalk5.yellow(`No countries matching "${query}".`));
566
+ } else {
567
+ const rows = matches.map((c) => [
568
+ c.iso2,
569
+ c.iso3,
570
+ c.name,
571
+ c.capital || "",
572
+ c.phonecode ? `+${c.phonecode.replace(/^\+/, "")}` : "",
573
+ c.currency || ""
574
+ ]);
575
+ printTable(["ISO2", "ISO3", "Name", "Capital", "Phone", "Currency"], rows);
576
+ }
577
+ if (!flags.json) {
578
+ process.stderr.write(
579
+ chalk5.dim("\nTip: Use `csc search states --country IN` to search within a country") + "\n"
580
+ );
581
+ }
582
+ printUsageFooter(usage, flags);
583
+ });
584
+ }
585
+
586
+ // src/commands/get.ts
587
+ import chalk6 from "chalk";
588
+ function tryParseJson(value) {
589
+ try {
590
+ return JSON.parse(value);
591
+ } catch {
592
+ return value;
593
+ }
594
+ }
595
+ function formatTimezones(raw) {
596
+ const parsed = tryParseJson(raw);
597
+ if (!Array.isArray(parsed)) return raw;
598
+ return parsed.map((tz) => {
599
+ const offset = tz.utcOffset || tz.gmtOffsetName || "";
600
+ const offsetStr = offset.startsWith("UTC") ? offset : `UTC${offset}`;
601
+ return `${tz.zoneName || ""} (${tz.abbreviation || ""}, ${offsetStr})`;
602
+ }).join(", ");
603
+ }
604
+ function formatCoord(value) {
605
+ const num = parseFloat(value);
606
+ return isNaN(num) ? value : num.toFixed(4);
607
+ }
608
+ function registerGetCommands(program2) {
609
+ const getCmd = program2.command("get").description("Get detailed information");
610
+ getCmd.command("country [iso2]").description("Get detailed country information").action(async (iso2, options, cmd) => {
611
+ const globalOpts = cmd.optsWithGlobals();
612
+ const flags = {
613
+ json: globalOpts.json ?? false,
614
+ quiet: globalOpts.quiet ?? false,
615
+ noFooter: globalOpts.footer === false
616
+ };
617
+ let code;
618
+ if (iso2) {
619
+ code = iso2.toUpperCase();
620
+ } else if (isTTY()) {
621
+ const countrySpinner = await createSpinner("Loading countries...", flags);
622
+ const { data: allCountries } = await get("/countries");
623
+ countrySpinner.stop();
624
+ code = await promptCountry(allCountries);
625
+ } else {
626
+ process.stderr.write(chalk6.red("Country ISO2 code required. Example: csc get country IN\n"));
627
+ process.exit(1);
628
+ return;
629
+ }
630
+ const spinner = await createSpinner(`Fetching ${code}...`, flags);
631
+ const { data, usage } = await get(`/countries/${code}`);
632
+ spinner.stop();
633
+ if (flags.json) {
634
+ printJson(data);
635
+ } else {
636
+ printDetail("Country:", data.name);
637
+ printDetail("ISO2:", data.iso2);
638
+ printDetail("ISO3:", data.iso3);
639
+ printDetail("Capital:", data.capital || "N/A");
640
+ printDetail("Phone Code:", `+${(data.phonecode || "").replace(/^\+/, "")}`);
641
+ const currencyParts = [data.currency, data.currency_name, data.currency_symbol].filter(Boolean);
642
+ printDetail("Currency:", currencyParts.length > 1 ? `${data.currency} (${data.currency_name}) ${data.currency_symbol}` : data.currency || "N/A");
643
+ printDetail("Region:", `${data.region}${data.subregion ? ` > ${data.subregion}` : ""}`);
644
+ printDetail("Coordinates:", `${formatCoord(data.latitude)}, ${formatCoord(data.longitude)}`);
645
+ if (data.tld) printDetail("TLD:", data.tld);
646
+ printDetail("Native Name:", data.native || "N/A");
647
+ printDetail("Flag:", data.emoji || "");
648
+ printDetail("Timezones:", formatTimezones(data.timezones));
649
+ }
650
+ printUsageFooter(usage, flags);
651
+ });
652
+ getCmd.command("state [country_iso2] [state_iso2]").description("Get detailed state information").action(async (countryIso2, stateIso2, options, cmd) => {
653
+ const globalOpts = cmd.optsWithGlobals();
654
+ const flags = {
655
+ json: globalOpts.json ?? false,
656
+ quiet: globalOpts.quiet ?? false,
657
+ noFooter: globalOpts.footer === false
658
+ };
659
+ let countryCode;
660
+ if (countryIso2) {
661
+ countryCode = countryIso2.toUpperCase();
662
+ } else if (isTTY()) {
663
+ const countrySpinner = await createSpinner("Loading countries...", flags);
664
+ const { data: allCountries } = await get("/countries");
665
+ countrySpinner.stop();
666
+ countryCode = await promptCountry(allCountries);
667
+ } else {
668
+ process.stderr.write(chalk6.red("Country ISO2 code required. Example: csc get state IN MH\n"));
669
+ process.exit(1);
670
+ return;
671
+ }
672
+ let stateCode;
673
+ if (stateIso2) {
674
+ stateCode = stateIso2.toUpperCase();
675
+ } else if (isTTY()) {
676
+ const stateSpinner = await createSpinner(`Loading states for ${countryCode}...`, flags);
677
+ const { data: allStates } = await get(`/countries/${countryCode}/states`);
678
+ stateSpinner.stop();
679
+ stateCode = await promptState(allStates);
680
+ } else {
681
+ process.stderr.write(chalk6.red("State ISO2 code required. Example: csc get state IN MH\n"));
682
+ process.exit(1);
683
+ return;
684
+ }
685
+ const spinner = await createSpinner(`Fetching ${countryCode}/${stateCode}...`, flags);
686
+ const { data, usage } = await get(
687
+ `/countries/${countryCode}/states/${stateCode}`
688
+ );
689
+ spinner.stop();
690
+ if (flags.json) {
691
+ printJson(data);
692
+ } else {
693
+ printDetail("State:", data.name);
694
+ printDetail("ISO2:", data.iso2);
695
+ printDetail("Country:", data.country_code);
696
+ if (data.type) printDetail("Type:", data.type);
697
+ if (data.latitude && data.longitude) {
698
+ printDetail("Coordinates:", `${formatCoord(data.latitude)}, ${formatCoord(data.longitude)}`);
699
+ }
700
+ }
701
+ printUsageFooter(usage, flags);
702
+ });
703
+ }
704
+
705
+ // src/commands/usage.ts
706
+ import chalk7 from "chalk";
707
+ function tierPrice(dailyLimit) {
708
+ if (dailyLimit <= 100) return "Free";
709
+ if (dailyLimit <= 300) return "$5/mo";
710
+ if (dailyLimit <= 1e3) return "$9/mo";
711
+ if (dailyLimit <= 3300) return "$29/mo";
712
+ if (dailyLimit <= 25e3) return "$79/mo";
713
+ return "Custom";
714
+ }
715
+ function daysUntilMonthlyReset() {
716
+ const now = /* @__PURE__ */ new Date();
717
+ const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
718
+ return Math.ceil((nextMonth.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
719
+ }
720
+ function registerUsageCommand(program2) {
721
+ program2.command("usage").description("View API usage and quota").action(async (_options, cmd) => {
722
+ const globalOpts = cmd.optsWithGlobals();
723
+ const flags = {
724
+ json: globalOpts.json ?? false,
725
+ quiet: globalOpts.quiet ?? false,
726
+ noFooter: globalOpts.footer === false
727
+ };
728
+ const spinner = await createSpinner("Fetching usage...", flags);
729
+ const { usage } = await get("/countries/IN");
730
+ spinner.stop();
731
+ if (!usage) {
732
+ if (flags.json) {
733
+ process.stdout.write(JSON.stringify({ error: "Usage information not available." }) + "\n");
734
+ } else {
735
+ console.log(chalk7.yellow("Usage information not available."));
736
+ }
737
+ return;
738
+ }
739
+ const { dailyUsed, dailyLimit, monthlyUsed, monthlyLimit } = usage;
740
+ if (dailyLimit === 0 || monthlyLimit === 0) {
741
+ if (flags.json) {
742
+ process.stdout.write(JSON.stringify({ error: "Usage limit data is unavailable." }) + "\n");
743
+ } else {
744
+ console.log(chalk7.yellow("Usage limit data is unavailable."));
745
+ }
746
+ return;
747
+ }
748
+ const tier = getTierName(dailyLimit);
749
+ const price = tierPrice(dailyLimit);
750
+ const dailyPct = Math.round(dailyUsed / dailyLimit * 100);
751
+ const monthlyPct = Math.round(monthlyUsed / monthlyLimit * 100);
752
+ if (flags.json) {
753
+ process.stdout.write(
754
+ JSON.stringify({
755
+ plan: tier,
756
+ price,
757
+ daily: { used: dailyUsed, limit: dailyLimit, percent: dailyPct },
758
+ monthly: { used: monthlyUsed, limit: monthlyLimit, percent: monthlyPct }
759
+ }) + "\n"
760
+ );
761
+ return;
762
+ }
763
+ console.log(`${chalk7.bold("Plan:".padEnd(12))}${tier} (${price})`);
764
+ console.log(
765
+ `${chalk7.bold("Daily:".padEnd(12))}${progressBar(dailyUsed, dailyLimit)} ${formatNumber(dailyUsed)} / ${formatNumber(dailyLimit)} (${dailyPct}%)`
766
+ );
767
+ console.log(
768
+ `${chalk7.bold("Monthly:".padEnd(12))}${progressBar(monthlyUsed, monthlyLimit)} ${formatNumber(monthlyUsed)} / ${formatNumber(monthlyLimit)} (${monthlyPct}%)`
769
+ );
770
+ console.log(
771
+ `${chalk7.bold("Resets:".padEnd(12))}Daily in ${timeUntilDailyReset()} | Monthly in ${daysUntilMonthlyReset()} days`
772
+ );
773
+ });
774
+ }
775
+
776
+ // src/commands/upgrade.ts
777
+ import chalk8 from "chalk";
778
+ import open from "open";
779
+ var PLANS = [
780
+ { name: "Community", price: "Free", daily: 100, monthly: 3e3 },
781
+ { name: "Starter", price: "$5/mo", daily: 300, monthly: 9e3 },
782
+ { name: "Supporter", price: "$9/mo", daily: 1e3, monthly: 3e4 },
783
+ { name: "Professional", price: "$29/mo", daily: 3300, monthly: 1e5 },
784
+ { name: "Business", price: "$79/mo", daily: 25e3, monthly: 75e4 }
785
+ ];
786
+ function registerUpgradeCommand(program2) {
787
+ program2.command("upgrade").description("View plans and open pricing page").action(async (_options, cmd) => {
788
+ const globalOpts = cmd.optsWithGlobals();
789
+ const flags = {
790
+ json: globalOpts.json ?? false,
791
+ quiet: globalOpts.quiet ?? false,
792
+ noFooter: globalOpts.footer === false
793
+ };
794
+ let usage = null;
795
+ let currentPlan;
796
+ const key = getApiKey();
797
+ if (key) {
798
+ const spinner = await createSpinner("Fetching current plan...", flags);
799
+ const result = await validateKey(key);
800
+ spinner.stop();
801
+ if (result.valid && result.usage) {
802
+ usage = result.usage;
803
+ currentPlan = getTierName(result.usage.dailyLimit);
804
+ }
805
+ }
806
+ if (flags.json) {
807
+ const output = { plans: PLANS };
808
+ if (currentPlan) output.currentPlan = currentPlan;
809
+ process.stdout.write(JSON.stringify(output) + "\n");
810
+ return;
811
+ }
812
+ if (currentPlan) {
813
+ console.log(`Current plan: ${chalk8.bold(currentPlan)}
814
+ `);
815
+ }
816
+ console.log("Available plans:\n");
817
+ printTable(
818
+ ["Plan", "Price", "Daily", "Monthly"],
819
+ PLANS.map((p) => [
820
+ p.name,
821
+ p.price,
822
+ p.daily.toLocaleString("en-US"),
823
+ p.monthly.toLocaleString("en-US")
824
+ ])
825
+ );
826
+ printUsageFooter(usage, flags);
827
+ console.log(`
828
+ ${chalk8.dim("Opening pricing page...")}`);
829
+ await open("https://app.countrystatecity.in/pricing");
830
+ });
831
+ }
832
+
833
+ // src/commands/generate.ts
834
+ import chalk9 from "chalk";
835
+ import { mkdir, writeFile } from "fs/promises";
836
+ import { join } from "path";
837
+
838
+ // src/templates/react-dropdown.ts
839
+ function generateCountryDropdown(countries, tsx) {
840
+ const ext = tsx ? "tsx" : "jsx";
841
+ const dataJson = countries.map(
842
+ (c) => ` { id: ${c.id}, name: ${JSON.stringify(c.name)}, iso2: ${JSON.stringify(c.iso2)}, phonecode: ${JSON.stringify(c.phonecode)}, emoji: ${JSON.stringify(c.emoji)} }`
843
+ ).join(",\n");
844
+ return `// CountrySelect.${ext}
845
+ // Generated by @countrystatecity/cli
846
+ // Data: ${countries.length} countries from countrystatecity.in
847
+
848
+ import { useState } from 'react';
849
+ ${tsx ? `
850
+ interface Country {
851
+ id: number;
852
+ name: string;
853
+ iso2: string;
854
+ phonecode: string;
855
+ emoji: string;
856
+ }
857
+
858
+ interface CountrySelectProps {
859
+ value?: string;
860
+ onChange?: (iso2: string, country: Country) => void;
861
+ placeholder?: string;
862
+ className?: string;
863
+ disabled?: boolean;
864
+ }
865
+ ` : ""}
866
+ const countries${tsx ? ": Country[]" : ""} = [
867
+ ${dataJson}
868
+ ];
869
+
870
+ export default function CountrySelect(${tsx ? `{
871
+ value,
872
+ onChange,
873
+ placeholder = "Select a country",
874
+ className = "",
875
+ disabled = false,
876
+ }: CountrySelectProps` : `{
877
+ value,
878
+ onChange,
879
+ placeholder = "Select a country",
880
+ className = "",
881
+ disabled = false,
882
+ }`}) {
883
+ const [internalSelected, setInternalSelected] = useState(value ?? "");
884
+ const selected = value ?? internalSelected;
885
+
886
+ const handleChange = (e${tsx ? ": React.ChangeEvent<HTMLSelectElement>" : ""}) => {
887
+ const iso2 = e.target.value;
888
+ setInternalSelected(iso2);
889
+ const country = countries.find(c => c.iso2 === iso2);
890
+ if (onChange && country) onChange(iso2, country);
891
+ };
892
+
893
+ return (
894
+ <select
895
+ value={selected}
896
+ onChange={handleChange}
897
+ className={className}
898
+ disabled={disabled}
899
+ >
900
+ <option value="">{placeholder}</option>
901
+ {countries.map((c) => (
902
+ <option key={c.iso2} value={c.iso2}>
903
+ {c.emoji} {c.name}
904
+ </option>
905
+ ))}
906
+ </select>
907
+ );
908
+ }
909
+ `;
910
+ }
911
+ function generateStateDropdown(states, countryCode, tsx) {
912
+ const ext = tsx ? "tsx" : "jsx";
913
+ const dataJson = states.map(
914
+ (s) => ` { id: ${s.id}, name: ${JSON.stringify(s.name)}, iso2: ${JSON.stringify(s.iso2)} }`
915
+ ).join(",\n");
916
+ return `// StateSelect.${ext}
917
+ // Generated by @countrystatecity/cli
918
+ // Data: ${states.length} states for ${countryCode} from countrystatecity.in
919
+
920
+ import { useState } from 'react';
921
+ ${tsx ? `
922
+ interface State {
923
+ id: number;
924
+ name: string;
925
+ iso2: string;
926
+ }
927
+
928
+ interface StateSelectProps {
929
+ countryCode?: string;
930
+ value?: string;
931
+ onChange?: (iso2: string, state: State) => void;
932
+ placeholder?: string;
933
+ className?: string;
934
+ disabled?: boolean;
935
+ }
936
+ ` : ""}
937
+ const states${tsx ? ": State[]" : ""} = [
938
+ ${dataJson}
939
+ ];
940
+
941
+ export default function StateSelect(${tsx ? `{
942
+ countryCode = "${countryCode}",
943
+ value,
944
+ onChange,
945
+ placeholder = "Select a state",
946
+ className = "",
947
+ disabled = false,
948
+ }: StateSelectProps` : `{
949
+ countryCode = "${countryCode}",
950
+ value,
951
+ onChange,
952
+ placeholder = "Select a state",
953
+ className = "",
954
+ disabled = false,
955
+ }`}) {
956
+ const [internalSelected, setInternalSelected] = useState(value ?? "");
957
+ const selected = value ?? internalSelected;
958
+
959
+ const handleChange = (e${tsx ? ": React.ChangeEvent<HTMLSelectElement>" : ""}) => {
960
+ const iso2 = e.target.value;
961
+ setInternalSelected(iso2);
962
+ const state = states.find(s => s.iso2 === iso2);
963
+ if (onChange && state) onChange(iso2, state);
964
+ };
965
+
966
+ return (
967
+ <select
968
+ value={selected}
969
+ onChange={handleChange}
970
+ className={className}
971
+ disabled={disabled}
972
+ data-country={countryCode}
973
+ >
974
+ <option value="">{placeholder}</option>
975
+ {states.map((s) => (
976
+ <option key={s.iso2} value={s.iso2}>
977
+ {s.name}
978
+ </option>
979
+ ))}
980
+ </select>
981
+ );
982
+ }
983
+ `;
984
+ }
985
+ function generateCityDropdown(cities, countryCode, stateCode, tsx) {
986
+ const ext = tsx ? "tsx" : "jsx";
987
+ const dataJson = cities.map((c) => ` { id: ${c.id}, name: ${JSON.stringify(c.name)} }`).join(",\n");
988
+ return `// CitySelect.${ext}
989
+ // Generated by @countrystatecity/cli
990
+ // Data: ${cities.length} cities for ${countryCode}/${stateCode} from countrystatecity.in
991
+
992
+ import { useState } from 'react';
993
+ ${tsx ? `
994
+ interface City {
995
+ id: number;
996
+ name: string;
997
+ }
998
+
999
+ interface CitySelectProps {
1000
+ countryCode?: string;
1001
+ stateCode?: string;
1002
+ value?: string;
1003
+ onChange?: (name: string, city: City) => void;
1004
+ placeholder?: string;
1005
+ className?: string;
1006
+ disabled?: boolean;
1007
+ }
1008
+ ` : ""}
1009
+ const cities${tsx ? ": City[]" : ""} = [
1010
+ ${dataJson}
1011
+ ];
1012
+
1013
+ export default function CitySelect(${tsx ? `{
1014
+ countryCode = "${countryCode}",
1015
+ stateCode = "${stateCode}",
1016
+ value,
1017
+ onChange,
1018
+ placeholder = "Select a city",
1019
+ className = "",
1020
+ disabled = false,
1021
+ }: CitySelectProps` : `{
1022
+ countryCode = "${countryCode}",
1023
+ stateCode = "${stateCode}",
1024
+ value,
1025
+ onChange,
1026
+ placeholder = "Select a city",
1027
+ className = "",
1028
+ disabled = false,
1029
+ }`}) {
1030
+ const [internalSelected, setInternalSelected] = useState(value ?? "");
1031
+ const selected = value ?? internalSelected;
1032
+
1033
+ const handleChange = (e${tsx ? ": React.ChangeEvent<HTMLSelectElement>" : ""}) => {
1034
+ const name = e.target.value;
1035
+ setInternalSelected(name);
1036
+ const city = cities.find(c => c.name === name);
1037
+ if (onChange && city) onChange(name, city);
1038
+ };
1039
+
1040
+ return (
1041
+ <select
1042
+ value={selected}
1043
+ onChange={handleChange}
1044
+ className={className}
1045
+ disabled={disabled}
1046
+ data-country={countryCode}
1047
+ data-state={stateCode}
1048
+ >
1049
+ <option value="">{placeholder}</option>
1050
+ {cities.map((c) => (
1051
+ <option key={c.id} value={c.name}>
1052
+ {c.name}
1053
+ </option>
1054
+ ))}
1055
+ </select>
1056
+ );
1057
+ }
1058
+ `;
1059
+ }
1060
+
1061
+ // src/templates/prisma-seed.ts
1062
+ function generateCountrySeed(countries) {
1063
+ const dataJson = countries.map(
1064
+ (c) => ` { name: ${JSON.stringify(c.name)}, iso2: ${JSON.stringify(c.iso2)}, iso3: ${JSON.stringify(c.iso3)}, phonecode: ${JSON.stringify(c.phonecode)}, capital: ${JSON.stringify(c.capital || "")}, currency: ${JSON.stringify(c.currency)} }`
1065
+ ).join(",\n");
1066
+ return `// seed-countries.ts
1067
+ // Generated by @countrystatecity/cli
1068
+ // Data: ${countries.length} countries from countrystatecity.in
1069
+
1070
+ import { PrismaClient } from '@prisma/client';
1071
+
1072
+ const prisma = new PrismaClient();
1073
+
1074
+ const countries = [
1075
+ ${dataJson}
1076
+ ];
1077
+
1078
+ async function main() {
1079
+ console.log(\`Seeding \${countries.length} countries...\`);
1080
+ await prisma.country.createMany({
1081
+ data: countries,
1082
+ skipDuplicates: true,
1083
+ });
1084
+ console.log('Done.');
1085
+ }
1086
+
1087
+ main()
1088
+ .catch(console.error)
1089
+ .finally(() => prisma.$disconnect());
1090
+ `;
1091
+ }
1092
+ function generateStateSeed(states, countryCode) {
1093
+ const dataJson = states.map(
1094
+ (s) => ` { name: ${JSON.stringify(s.name)}, iso2: ${JSON.stringify(s.iso2)}, countryCode: ${JSON.stringify(s.country_code)} }`
1095
+ ).join(",\n");
1096
+ return `// seed-states.ts
1097
+ // Generated by @countrystatecity/cli
1098
+ // Data: ${states.length} states for ${countryCode} from countrystatecity.in
1099
+
1100
+ import { PrismaClient } from '@prisma/client';
1101
+
1102
+ const prisma = new PrismaClient();
1103
+
1104
+ const states = [
1105
+ ${dataJson}
1106
+ ];
1107
+
1108
+ async function main() {
1109
+ console.log(\`Seeding \${states.length} states...\`);
1110
+ await prisma.state.createMany({
1111
+ data: states,
1112
+ skipDuplicates: true,
1113
+ });
1114
+ console.log('Done.');
1115
+ }
1116
+
1117
+ main()
1118
+ .catch(console.error)
1119
+ .finally(() => prisma.$disconnect());
1120
+ `;
1121
+ }
1122
+ function generateCitySeed(cities, countryCode, stateCode) {
1123
+ const dataJson = cities.map((c) => ` { name: ${JSON.stringify(c.name)} }`).join(",\n");
1124
+ return `// seed-cities.ts
1125
+ // Generated by @countrystatecity/cli
1126
+ // Data: ${cities.length} cities for ${countryCode}/${stateCode} from countrystatecity.in
1127
+
1128
+ import { PrismaClient } from '@prisma/client';
1129
+
1130
+ const prisma = new PrismaClient();
1131
+
1132
+ const cities = [
1133
+ ${dataJson}
1134
+ ];
1135
+
1136
+ async function main() {
1137
+ console.log(\`Seeding \${cities.length} cities...\`);
1138
+ await prisma.city.createMany({
1139
+ data: cities,
1140
+ skipDuplicates: true,
1141
+ });
1142
+ console.log('Done.');
1143
+ }
1144
+
1145
+ main()
1146
+ .catch(console.error)
1147
+ .finally(() => prisma.$disconnect());
1148
+ `;
1149
+ }
1150
+
1151
+ // src/commands/generate.ts
1152
+ function enforceTierGate(usage, spinner) {
1153
+ if (!usage || usage.dailyLimit < 1e3) {
1154
+ spinner.fail("Tier check failed.");
1155
+ console.log(
1156
+ chalk9.yellow("The generate command requires a Supporter plan or above ($9/mo).")
1157
+ );
1158
+ if (usage) {
1159
+ const tierName = usage.dailyLimit <= 100 ? "Community (Free)" : "Starter ($5/mo)";
1160
+ console.log(`Your current plan: ${chalk9.bold(tierName)}
1161
+ `);
1162
+ } else {
1163
+ console.log(chalk9.dim("Could not verify your plan. Ensure usage headers are available.\n"));
1164
+ }
1165
+ console.log(chalk9.dim("Run `csc upgrade` to unlock code generation."));
1166
+ process.exit(1);
1167
+ }
1168
+ }
1169
+ function registerGenerateCommands(program2) {
1170
+ const generate = program2.command("generate").description("Generate code from API data");
1171
+ generate.command("dropdown").description("Generate a dropdown/select component").requiredOption("-e, --entity <type>", "Entity type: countries, states, or cities").requiredOption("-f, --format <format>", "Output format: react").option("-c, --country <iso2>", "Country ISO2 code (required for states/cities)").option("-s, --state <iso2>", "State ISO2 code (required for cities)").option("-o, --output <dir>", "Output directory", process.cwd()).option("--typescript", "Generate TypeScript (.tsx)", true).option("--no-typescript", "Generate JavaScript (.jsx)").action(
1172
+ async (options, cmd) => {
1173
+ const globalOpts = cmd.optsWithGlobals();
1174
+ const flags = {
1175
+ json: globalOpts.json ?? false,
1176
+ quiet: globalOpts.quiet ?? false,
1177
+ noFooter: globalOpts.footer === false
1178
+ };
1179
+ if (options.format !== "react") {
1180
+ process.stderr.write(chalk9.red(`Unsupported format: ${options.format}`) + "\n");
1181
+ process.stderr.write(chalk9.dim("Supported formats: react") + "\n");
1182
+ process.exit(1);
1183
+ }
1184
+ const spinner = await createSpinner("Fetching data...", flags);
1185
+ const tsx = options.typescript;
1186
+ const ext = tsx ? "tsx" : "jsx";
1187
+ let content;
1188
+ let filename;
1189
+ let fetchUsage = null;
1190
+ if (options.entity === "countries") {
1191
+ const { data, usage } = await get("/countries");
1192
+ fetchUsage = usage;
1193
+ enforceTierGate(usage, spinner);
1194
+ content = generateCountryDropdown(data, tsx);
1195
+ filename = `CountrySelect.${ext}`;
1196
+ } else if (options.entity === "states") {
1197
+ if (!options.country) {
1198
+ spinner.fail("Country code required for states.");
1199
+ process.stderr.write(chalk9.dim("Use --country IN") + "\n");
1200
+ process.exit(1);
1201
+ }
1202
+ const code = options.country.toUpperCase();
1203
+ const { data, usage } = await get(`/countries/${code}/states`);
1204
+ fetchUsage = usage;
1205
+ enforceTierGate(usage, spinner);
1206
+ content = generateStateDropdown(data, code, tsx);
1207
+ filename = `StateSelect.${ext}`;
1208
+ } else if (options.entity === "cities") {
1209
+ if (!options.country || !options.state) {
1210
+ spinner.fail("Country and state codes required for cities.");
1211
+ process.stderr.write(chalk9.dim("Use --country IN --state MH") + "\n");
1212
+ process.exit(1);
1213
+ }
1214
+ const countryCode = options.country.toUpperCase();
1215
+ const stateCode = options.state.toUpperCase();
1216
+ const { data, usage } = await get(
1217
+ `/countries/${countryCode}/states/${stateCode}/cities`
1218
+ );
1219
+ fetchUsage = usage;
1220
+ enforceTierGate(usage, spinner);
1221
+ content = generateCityDropdown(data, countryCode, stateCode, tsx);
1222
+ filename = `CitySelect.${ext}`;
1223
+ } else {
1224
+ spinner.fail(`Unknown entity: ${options.entity}`);
1225
+ process.stderr.write(chalk9.dim("Supported entities: countries, states, cities") + "\n");
1226
+ process.exit(1);
1227
+ }
1228
+ try {
1229
+ await mkdir(options.output, { recursive: true });
1230
+ const filepath = join(options.output, filename);
1231
+ await writeFile(filepath, content, "utf-8");
1232
+ spinner.succeed(`Generated ${chalk9.bold(filepath)}`);
1233
+ } catch (err) {
1234
+ spinner.fail("Failed to write output file.");
1235
+ process.stderr.write(chalk9.red(String(err)) + "\n");
1236
+ process.exit(1);
1237
+ }
1238
+ printUsageFooter(fetchUsage, flags);
1239
+ }
1240
+ );
1241
+ generate.command("seed").description("Generate a database seed file").requiredOption("-e, --entity <type>", "Entity type: countries, states, or cities").requiredOption("-f, --format <format>", "Output format: prisma").option("-c, --country <iso2>", "Country ISO2 code (for states/cities)").option("-s, --state <iso2>", "State ISO2 code (for cities)").option("-o, --output <dir>", "Output directory", process.cwd()).action(
1242
+ async (options, cmd) => {
1243
+ const globalOpts = cmd.optsWithGlobals();
1244
+ const flags = {
1245
+ json: globalOpts.json ?? false,
1246
+ quiet: globalOpts.quiet ?? false,
1247
+ noFooter: globalOpts.footer === false
1248
+ };
1249
+ if (options.format !== "prisma") {
1250
+ process.stderr.write(chalk9.red(`Unsupported format: ${options.format}`) + "\n");
1251
+ process.stderr.write(chalk9.dim("Supported formats: prisma") + "\n");
1252
+ process.exit(1);
1253
+ }
1254
+ const spinner = await createSpinner("Fetching data...", flags);
1255
+ let content;
1256
+ let filename;
1257
+ let fetchUsage = null;
1258
+ if (options.entity === "countries") {
1259
+ const { data, usage } = await get("/countries");
1260
+ fetchUsage = usage;
1261
+ enforceTierGate(usage, spinner);
1262
+ content = generateCountrySeed(data);
1263
+ filename = "seed-countries.ts";
1264
+ } else if (options.entity === "states") {
1265
+ if (!options.country) {
1266
+ spinner.fail("Country code required for states.");
1267
+ process.stderr.write(chalk9.dim("Use --country IN") + "\n");
1268
+ process.exit(1);
1269
+ }
1270
+ const code = options.country.toUpperCase();
1271
+ const { data, usage } = await get(`/countries/${code}/states`);
1272
+ fetchUsage = usage;
1273
+ enforceTierGate(usage, spinner);
1274
+ content = generateStateSeed(data, code);
1275
+ filename = "seed-states.ts";
1276
+ } else if (options.entity === "cities") {
1277
+ if (!options.country || !options.state) {
1278
+ spinner.fail("Country and state codes required for cities.");
1279
+ process.stderr.write(chalk9.dim("Use --country IN --state MH") + "\n");
1280
+ process.exit(1);
1281
+ }
1282
+ const countryCode = options.country.toUpperCase();
1283
+ const stateCode = options.state.toUpperCase();
1284
+ const { data, usage } = await get(
1285
+ `/countries/${countryCode}/states/${stateCode}/cities`
1286
+ );
1287
+ fetchUsage = usage;
1288
+ enforceTierGate(usage, spinner);
1289
+ content = generateCitySeed(data, countryCode, stateCode);
1290
+ filename = "seed-cities.ts";
1291
+ } else {
1292
+ spinner.fail(`Unknown entity: ${options.entity}`);
1293
+ process.stderr.write(chalk9.dim("Supported entities: countries, states, cities") + "\n");
1294
+ process.exit(1);
1295
+ }
1296
+ try {
1297
+ await mkdir(options.output, { recursive: true });
1298
+ const filepath = join(options.output, filename);
1299
+ await writeFile(filepath, content, "utf-8");
1300
+ spinner.succeed(`Generated ${chalk9.bold(filepath)}`);
1301
+ } catch (err) {
1302
+ spinner.fail("Failed to write output file.");
1303
+ process.stderr.write(chalk9.red(String(err)) + "\n");
1304
+ process.exit(1);
1305
+ }
1306
+ printUsageFooter(fetchUsage, flags);
1307
+ }
1308
+ );
1309
+ }
1310
+
1311
+ // src/commands/explore.ts
1312
+ import select from "@inquirer/select";
1313
+ import search2 from "@inquirer/search";
1314
+ var ACTION_BACK = "back";
1315
+ async function promptCountry2(countries) {
1316
+ return search2({
1317
+ message: "Select a country",
1318
+ source: (input) => {
1319
+ const query = (input ?? "").toLowerCase();
1320
+ return Promise.resolve(
1321
+ countries.filter(
1322
+ (c) => c.name.toLowerCase().includes(query) || c.iso2.toLowerCase().includes(query)
1323
+ ).map((c) => ({
1324
+ name: `${c.emoji} ${c.name} (${c.iso2})`,
1325
+ value: c.iso2
1326
+ }))
1327
+ );
1328
+ }
1329
+ });
1330
+ }
1331
+ async function promptState2(states) {
1332
+ return search2({
1333
+ message: "Select a state",
1334
+ source: (input) => {
1335
+ const query = (input ?? "").toLowerCase();
1336
+ return Promise.resolve(
1337
+ states.filter(
1338
+ (s) => s.name.toLowerCase().includes(query) || (s.iso2 ?? "").toLowerCase().includes(query)
1339
+ ).map((s) => ({ name: s.name, value: s.iso2 ?? "" }))
1340
+ );
1341
+ }
1342
+ });
1343
+ }
1344
+ async function promptAction(countryIso, stateName) {
1345
+ return select({
1346
+ message: `Actions for ${countryIso} / ${stateName}`,
1347
+ choices: [
1348
+ { name: "View cities", value: "cities" },
1349
+ { name: "View country details", value: "country-detail" },
1350
+ { name: "View state details", value: "state-detail" },
1351
+ { name: "Generate dropdown (hint)", value: "generate-dropdown" },
1352
+ { name: "Generate seed (hint)", value: "generate-seed" },
1353
+ { name: "Go back", value: ACTION_BACK }
1354
+ ]
1355
+ });
1356
+ }
1357
+ async function handleViewCities(countryIso, stateIso) {
1358
+ const { data: cities, usage } = await get(
1359
+ `/countries/${countryIso}/states/${stateIso}/cities`
1360
+ );
1361
+ if (cities.length === 0) {
1362
+ stderr("No cities found for this state.");
1363
+ return usage;
1364
+ }
1365
+ const rows = cities.map((c) => [String(c.id), c.name]);
1366
+ printTable(["ID", "Name"], rows);
1367
+ return usage;
1368
+ }
1369
+ async function handleCountryDetail(countryIso) {
1370
+ const { data, usage } = await get(`/countries/${countryIso}`);
1371
+ printJson(data);
1372
+ return usage;
1373
+ }
1374
+ async function handleStateDetail(countryIso, stateIso) {
1375
+ const { data, usage } = await get(
1376
+ `/countries/${countryIso}/states/${stateIso}`
1377
+ );
1378
+ printJson(data);
1379
+ return usage;
1380
+ }
1381
+ function handleGenerateDropdown(countryIso) {
1382
+ stderr(`Run: csc generate dropdown -e states -f react -c ${countryIso}`);
1383
+ }
1384
+ function handleGenerateSeed(countryIso) {
1385
+ stderr(`Run: csc generate seed -e states -f prisma -c ${countryIso}`);
1386
+ }
1387
+ async function runExploreSession(flags) {
1388
+ const { data: countries, usage: countriesUsage } = await get("/countries");
1389
+ let latestUsage = countriesUsage;
1390
+ const countryIso = await promptCountry2(countries);
1391
+ const { data: states, usage: statesUsage } = await get(
1392
+ `/countries/${countryIso}/states`
1393
+ );
1394
+ latestUsage = statesUsage ?? latestUsage;
1395
+ if (states.length === 0) {
1396
+ stderr(`No states found for ${countryIso}.`);
1397
+ return latestUsage;
1398
+ }
1399
+ const stateIso = await promptState2(states);
1400
+ const selectedState = states.find((s) => s.iso2 === stateIso);
1401
+ const stateName = selectedState?.name ?? stateIso;
1402
+ let running = true;
1403
+ while (running) {
1404
+ const action = await promptAction(countryIso, stateName);
1405
+ if (action === ACTION_BACK) {
1406
+ running = false;
1407
+ break;
1408
+ }
1409
+ if (action === "cities") {
1410
+ const usage = await handleViewCities(countryIso, stateIso);
1411
+ latestUsage = usage ?? latestUsage;
1412
+ } else if (action === "country-detail") {
1413
+ const usage = await handleCountryDetail(countryIso);
1414
+ latestUsage = usage ?? latestUsage;
1415
+ } else if (action === "state-detail") {
1416
+ const usage = await handleStateDetail(countryIso, stateIso);
1417
+ latestUsage = usage ?? latestUsage;
1418
+ } else if (action === "generate-dropdown") {
1419
+ handleGenerateDropdown(countryIso);
1420
+ } else if (action === "generate-seed") {
1421
+ handleGenerateSeed(countryIso);
1422
+ }
1423
+ }
1424
+ return latestUsage;
1425
+ }
1426
+ function registerExploreCommand(program2) {
1427
+ program2.command("explore").description("Interactive geographic data browser").action(async (_options, cmd) => {
1428
+ if (!process.stdin.isTTY) {
1429
+ stderr("Error: `csc explore` requires an interactive terminal (TTY).");
1430
+ stderr("Use `csc search` for non-interactive workflows.");
1431
+ process.exit(1);
1432
+ }
1433
+ const globalOpts = cmd.optsWithGlobals();
1434
+ const flags = {
1435
+ json: globalOpts.json ?? false,
1436
+ quiet: globalOpts.quiet ?? false,
1437
+ noFooter: globalOpts.footer === false
1438
+ };
1439
+ const usage = await runExploreSession(flags);
1440
+ printUsageFooter(usage, flags);
1441
+ });
1442
+ }
1443
+
1444
+ // src/commands/export.ts
1445
+ import chalk10 from "chalk";
1446
+ import open2 from "open";
1447
+ var EXPORT_URL = "https://export.countrystatecity.in";
1448
+ function registerExportCommand(program2) {
1449
+ program2.command("export").description("Open the export tool in your browser").action(async function() {
1450
+ const globals = this.optsWithGlobals();
1451
+ if (globals.json) {
1452
+ console.log(JSON.stringify({ url: EXPORT_URL }));
1453
+ return;
1454
+ }
1455
+ console.log(chalk10.dim("Opening the CountryStateCity export tool..."));
1456
+ console.log(chalk10.dim(`URL: ${EXPORT_URL}`));
1457
+ await open2(EXPORT_URL);
1458
+ });
1459
+ }
1460
+
1461
+ // src/lib/branding.ts
1462
+ import chalk11 from "chalk";
1463
+ var VERSION = "v0.1.1";
1464
+ var GRADIENT_COLORS = [
1465
+ "#2296f3",
1466
+ // row 1 — blue
1467
+ "#2ba8e8",
1468
+ // row 2 — light blue
1469
+ "#4dbe9e",
1470
+ // row 3 — teal
1471
+ "#8ecf5e",
1472
+ // row 4 — lime-green
1473
+ "#cddc39"
1474
+ // row 5 — lime
1475
+ ];
1476
+ var ASCII_ROWS = [
1477
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588",
1478
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588",
1479
+ " \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
1480
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588",
1481
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588"
1482
+ ];
1483
+ function getAsciiArt() {
1484
+ const coloredRows = ASCII_ROWS.map(
1485
+ (row, index) => chalk11.hex(GRADIENT_COLORS[index])(row)
1486
+ );
1487
+ const subtitle = chalk11.dim(" Country State City CLI ") + chalk11.dim(VERSION);
1488
+ return coloredRows.join("\n") + "\n\n" + subtitle;
1489
+ }
1490
+ function formatCommand(name, description) {
1491
+ const paddedName = name.padEnd(12);
1492
+ return ` ${chalk11.hex("#2296f3")(paddedName)}${chalk11.dim(description)}`;
1493
+ }
1494
+ function formatFlag(flag, description) {
1495
+ const paddedFlag = flag.padEnd(16);
1496
+ return ` ${paddedFlag}${chalk11.dim(description)}`;
1497
+ }
1498
+ function getBrandedHelp() {
1499
+ const sectionHeader = (title) => chalk11.hex("#f97316")(title);
1500
+ const commands = [
1501
+ ["auth", "Save or remove your API key"],
1502
+ ["search", "Search countries, states, or cities by name"],
1503
+ ["get", "Fetch a specific country, state, or city by ISO code"],
1504
+ ["explore", "Interactively browse geographic data"],
1505
+ ["usage", "Show your current API usage and tier"],
1506
+ ["upgrade", "View plans and open the pricing page"],
1507
+ ["generate", "Generate dropdown or seed data for a framework"],
1508
+ ["export", "Export geographic data to JSON or CSV"]
1509
+ ];
1510
+ const flags = [
1511
+ ["--json", "Output raw JSON instead of formatted tables"],
1512
+ ["--quiet", "Suppress all decorative output"],
1513
+ ["--no-footer", "Hide the usage footer after each command"]
1514
+ ];
1515
+ const lines = [
1516
+ getAsciiArt(),
1517
+ "",
1518
+ sectionHeader("COMMANDS"),
1519
+ "",
1520
+ ...commands.map(([name, desc]) => formatCommand(name, desc)),
1521
+ "",
1522
+ sectionHeader("GLOBAL FLAGS"),
1523
+ "",
1524
+ ...flags.map(([flag, desc]) => formatFlag(flag, desc)),
1525
+ "",
1526
+ chalk11.dim("Run csc <command> --help for details")
1527
+ ];
1528
+ return lines.join("\n");
1529
+ }
1530
+
1531
+ // src/lib/root-help.ts
1532
+ var ROOT_COMMANDS = /* @__PURE__ */ new Set([
1533
+ "auth",
1534
+ "search",
1535
+ "get",
1536
+ "usage",
1537
+ "upgrade",
1538
+ "generate",
1539
+ "explore",
1540
+ "export"
1541
+ ]);
1542
+ function isRootHelpRequested(argv) {
1543
+ const args = argv.slice(2);
1544
+ if (args.length === 0) {
1545
+ return true;
1546
+ }
1547
+ const helpRequested = args.includes("-h") || args.includes("--help");
1548
+ if (!helpRequested) {
1549
+ return false;
1550
+ }
1551
+ const commandArg = args.find((arg) => !arg.startsWith("-"));
1552
+ return commandArg === void 0 || !ROOT_COMMANDS.has(commandArg);
1553
+ }
1554
+ function shouldShowBrandedHelp(argv) {
1555
+ const args = argv.slice(2);
1556
+ return !args.includes("--json") && !args.includes("-q") && !args.includes("--quiet");
1557
+ }
1558
+
1559
+ // src/index.ts
1560
+ function showBrandedHelpAndExit() {
1561
+ process.stdout.write(getBrandedHelp() + "\n");
1562
+ process.exit(0);
1563
+ }
1564
+ var program = new Command();
1565
+ program.addHelpCommand(false);
1566
+ program.name("csc").description("Official CLI for the Country State City API").version("0.1.1").helpOption("-h, --help", "Display help for csc").option("--json", "Output raw JSON instead of formatted tables", false).option("-q, --quiet", "Suppress all decorative output", false).option("--no-footer", "Hide the usage footer after each command");
1567
+ registerAuthCommands(program);
1568
+ registerSearchCommands(program);
1569
+ registerGetCommands(program);
1570
+ registerUsageCommand(program);
1571
+ registerUpgradeCommand(program);
1572
+ registerGenerateCommands(program);
1573
+ registerExploreCommand(program);
1574
+ registerExportCommand(program);
1575
+ if (isRootHelpRequested(process.argv) && shouldShowBrandedHelp(process.argv)) {
1576
+ showBrandedHelpAndExit();
1577
+ }
1578
+ program.parse();