shorter.sh 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.
package/dist/cli.js ADDED
@@ -0,0 +1,755 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var ShorterError = class extends Error {
5
+ status;
6
+ code;
7
+ constructor(message, status, code) {
8
+ super(message);
9
+ this.name = "ShorterError";
10
+ this.status = status;
11
+ this.code = code;
12
+ }
13
+ };
14
+ var ValidationError = class extends ShorterError {
15
+ constructor(message, code = "VALIDATION_ERROR") {
16
+ super(message, 400, code);
17
+ this.name = "ValidationError";
18
+ }
19
+ };
20
+ var AuthenticationError = class extends ShorterError {
21
+ constructor(message, code = "AUTH_REQUIRED") {
22
+ super(message, 401, code);
23
+ this.name = "AuthenticationError";
24
+ }
25
+ };
26
+ var ForbiddenError = class extends ShorterError {
27
+ constructor(message, code = "FORBIDDEN") {
28
+ super(message, 403, code);
29
+ this.name = "ForbiddenError";
30
+ }
31
+ };
32
+ var NotFoundError = class extends ShorterError {
33
+ constructor(message, code = "NOT_FOUND") {
34
+ super(message, 404, code);
35
+ this.name = "NotFoundError";
36
+ }
37
+ };
38
+ var RateLimitError = class extends ShorterError {
39
+ constructor(message, code = "RATE_LIMITED") {
40
+ super(message, 429, code);
41
+ this.name = "RateLimitError";
42
+ }
43
+ };
44
+ var ServerError = class extends ShorterError {
45
+ constructor(message, code = "SERVER_ERROR") {
46
+ super(message, 500, code);
47
+ this.name = "ServerError";
48
+ }
49
+ };
50
+ var NetworkError = class extends ShorterError {
51
+ constructor(message) {
52
+ super(message, 0, "NETWORK_ERROR");
53
+ this.name = "NetworkError";
54
+ }
55
+ };
56
+ function mapStatusToError(status, message, code) {
57
+ switch (status) {
58
+ case 400:
59
+ return new ValidationError(message, code);
60
+ case 401:
61
+ return new AuthenticationError(message, code);
62
+ case 403:
63
+ return new ForbiddenError(message, code);
64
+ case 404:
65
+ return new NotFoundError(message, code);
66
+ case 429:
67
+ return new RateLimitError(message, code);
68
+ default:
69
+ if (status >= 500) return new ServerError(message, code);
70
+ return new ShorterError(message, status, code);
71
+ }
72
+ }
73
+
74
+ // src/fetch-wrapper.ts
75
+ var FetchWrapper = class {
76
+ baseUrl;
77
+ apiKey;
78
+ fetchFn;
79
+ constructor(baseUrl, apiKey, fetchFn) {
80
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
81
+ this.apiKey = apiKey;
82
+ this.fetchFn = fetchFn;
83
+ }
84
+ async request(path2, options = {}) {
85
+ const { method = "GET", body, params } = options;
86
+ let url = `${this.baseUrl}${path2}`;
87
+ if (params) {
88
+ const searchParams = new URLSearchParams();
89
+ for (const [key, value] of Object.entries(params)) {
90
+ if (value !== void 0) {
91
+ searchParams.set(key, String(value));
92
+ }
93
+ }
94
+ const qs = searchParams.toString();
95
+ if (qs) url += `?${qs}`;
96
+ }
97
+ const headers = {
98
+ "Authorization": `Bearer ${this.apiKey}`,
99
+ "Accept": "application/json"
100
+ };
101
+ const init = { method, headers };
102
+ if (body) {
103
+ headers["Content-Type"] = "application/json";
104
+ init.body = JSON.stringify(body);
105
+ }
106
+ let response;
107
+ try {
108
+ response = await this.fetchFn(url, init);
109
+ } catch (err) {
110
+ throw new NetworkError(
111
+ err instanceof Error ? err.message : "Network request failed"
112
+ );
113
+ }
114
+ const data = await response.json();
115
+ if (!response.ok || data.success === false) {
116
+ throw mapStatusToError(
117
+ response.status,
118
+ data.message || `Request failed with status ${response.status}`,
119
+ data.code || "UNKNOWN_ERROR"
120
+ );
121
+ }
122
+ return data;
123
+ }
124
+ };
125
+
126
+ // src/analytics.ts
127
+ function mapTimeseries(raw) {
128
+ return raw.map((p) => ({
129
+ period: p.period,
130
+ clicks: p.clicks,
131
+ uniqueVisitors: p.unique_visitors
132
+ }));
133
+ }
134
+ function mapTopUrl(raw) {
135
+ return {
136
+ shortCode: raw.short_code,
137
+ shortUrl: raw.short_url,
138
+ originalUrl: raw.original_url,
139
+ clicks: raw.clicks
140
+ };
141
+ }
142
+ var AnalyticsClient = class {
143
+ fetch;
144
+ constructor(fetchWrapper) {
145
+ this.fetch = fetchWrapper;
146
+ }
147
+ async overview(options) {
148
+ const raw = await this.fetch.request("/api/v1/analytics/overview", {
149
+ params: {
150
+ start: options?.start !== void 0 ? String(options.start) : void 0,
151
+ end: options?.end !== void 0 ? String(options.end) : void 0
152
+ }
153
+ });
154
+ const timeseries = raw.timeseries;
155
+ const topUrls = raw.topUrls;
156
+ return {
157
+ totalClicks: raw.totalClicks,
158
+ uniqueVisitors: raw.uniqueVisitors,
159
+ prevPeriodClicks: raw.prevPeriodClicks,
160
+ prevPeriodUnique: raw.prevPeriodUnique,
161
+ timeseries: {
162
+ granularity: timeseries.granularity,
163
+ data: mapTimeseries(timeseries.data)
164
+ },
165
+ topUrls: topUrls.map(mapTopUrl),
166
+ countryBreakdown: raw.countryBreakdown,
167
+ deviceBreakdown: raw.deviceBreakdown,
168
+ browserBreakdown: raw.browserBreakdown,
169
+ osBreakdown: raw.osBreakdown,
170
+ referrerBreakdown: raw.referrerBreakdown
171
+ };
172
+ }
173
+ async url(shortCode, options) {
174
+ const raw = await this.fetch.request(`/api/v1/analytics/${shortCode}`, {
175
+ params: {
176
+ start: options?.start !== void 0 ? String(options.start) : void 0,
177
+ end: options?.end !== void 0 ? String(options.end) : void 0,
178
+ dimension: options?.dimension,
179
+ limit: options?.limit,
180
+ detail: options?.detail ? "true" : void 0
181
+ }
182
+ });
183
+ const timeseries = raw.timeseries;
184
+ const mappedTimeseries = {
185
+ granularity: timeseries.granularity,
186
+ data: mapTimeseries(timeseries.data)
187
+ };
188
+ if (options?.detail) {
189
+ return {
190
+ url: raw.url,
191
+ summary: raw.summary,
192
+ timeseries: mappedTimeseries,
193
+ breakdowns: raw.breakdowns
194
+ };
195
+ }
196
+ const result = {
197
+ summary: raw.summary,
198
+ timeseries: mappedTimeseries
199
+ };
200
+ if (raw.breakdown) {
201
+ result.breakdown = raw.breakdown;
202
+ }
203
+ return result;
204
+ }
205
+ };
206
+
207
+ // src/client.ts
208
+ function mapUrl(raw) {
209
+ return {
210
+ id: raw.id,
211
+ shortCode: raw.short_code,
212
+ shortUrl: raw.short_url,
213
+ originalUrl: raw.original_url,
214
+ clickCount: raw.click_count,
215
+ createdAt: new Date(raw.created_at).toISOString()
216
+ };
217
+ }
218
+ var ShorterClient = class {
219
+ analytics;
220
+ fetch;
221
+ constructor(options) {
222
+ const apiKey = options?.apiKey || (typeof process !== "undefined" ? process.env.SHORTER_API_KEY : void 0);
223
+ if (!apiKey) {
224
+ throw new AuthenticationError(
225
+ "API key is required. Pass it as options.apiKey or set SHORTER_API_KEY environment variable.",
226
+ "AUTH_REQUIRED"
227
+ );
228
+ }
229
+ if (!apiKey.startsWith("sk_")) {
230
+ throw new AuthenticationError(
231
+ 'Invalid API key format. Keys must start with "sk_".',
232
+ "INVALID_API_KEY"
233
+ );
234
+ }
235
+ const baseUrl = options?.baseUrl || "https://shorter.sh";
236
+ const fetchFn = options?.fetch || globalThis.fetch;
237
+ this.fetch = new FetchWrapper(baseUrl, apiKey, fetchFn);
238
+ this.analytics = new AnalyticsClient(this.fetch);
239
+ }
240
+ async shorten(url) {
241
+ const data = await this.fetch.request("/api/v1/shorten", {
242
+ method: "POST",
243
+ body: { url }
244
+ });
245
+ return {
246
+ shortCode: data.shortCode,
247
+ shortUrl: data.shortUrl,
248
+ originalUrl: data.originalUrl
249
+ };
250
+ }
251
+ async list(options) {
252
+ const data = await this.fetch.request("/api/v1/urls", {
253
+ params: {
254
+ page: options?.page,
255
+ limit: options?.limit
256
+ }
257
+ });
258
+ return {
259
+ urls: data.data.map(mapUrl),
260
+ pagination: data.pagination,
261
+ totalClicks: data.totalClicks
262
+ };
263
+ }
264
+ async delete(shortCode) {
265
+ const data = await this.fetch.request(`/api/v1/urls/${shortCode}`, {
266
+ method: "DELETE"
267
+ });
268
+ return { message: data.message };
269
+ }
270
+ };
271
+
272
+ // src/cli/config.ts
273
+ import * as fs from "fs";
274
+ import * as path from "path";
275
+ import * as os from "os";
276
+ function getConfigDir() {
277
+ if (process.platform === "win32") {
278
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
279
+ return path.join(appData, "shorter");
280
+ }
281
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
282
+ return path.join(xdg, "shorter");
283
+ }
284
+ function getConfigPath() {
285
+ return path.join(getConfigDir(), "config.json");
286
+ }
287
+ function loadConfig() {
288
+ const configPath = getConfigPath();
289
+ try {
290
+ const raw = fs.readFileSync(configPath, "utf-8");
291
+ return JSON.parse(raw);
292
+ } catch {
293
+ return {};
294
+ }
295
+ }
296
+ function saveConfig(config) {
297
+ const dir = getConfigDir();
298
+ fs.mkdirSync(dir, { recursive: true });
299
+ const configPath = getConfigPath();
300
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", {
301
+ mode: 384
302
+ });
303
+ }
304
+ function resolveApiKey() {
305
+ return process.env.SHORTER_API_KEY || loadConfig().apiKey;
306
+ }
307
+ function resolveBaseUrl() {
308
+ return process.env.SHORTER_BASE_URL || loadConfig().baseUrl || "https://shorter.sh";
309
+ }
310
+
311
+ // src/cli/prompt.ts
312
+ import * as readline from "readline";
313
+ function prompt(question) {
314
+ if (!process.stdin.isTTY) return Promise.resolve(null);
315
+ return new Promise((resolve) => {
316
+ const rl = readline.createInterface({
317
+ input: process.stdin,
318
+ output: process.stdout
319
+ });
320
+ rl.question(question, (answer) => {
321
+ rl.close();
322
+ resolve(answer.trim());
323
+ });
324
+ });
325
+ }
326
+ async function confirm(question) {
327
+ const answer = await prompt(`${question} [y/N] `);
328
+ return answer !== null && /^y(es)?$/i.test(answer);
329
+ }
330
+
331
+ // src/cli/output.ts
332
+ var isColorSupported = !process.env.NO_COLOR && process.stdout.isTTY;
333
+ function wrap(code, text) {
334
+ return isColorSupported ? `\x1B[${code}m${text}\x1B[0m` : text;
335
+ }
336
+ var bold = (s) => wrap("1", s);
337
+ var dim = (s) => wrap("2", s);
338
+ var green = (s) => wrap("32", s);
339
+ var red = (s) => wrap("31", s);
340
+ var yellow = (s) => wrap("33", s);
341
+ var cyan = (s) => wrap("36", s);
342
+ function success(msg) {
343
+ console.log(`${green("\u2713")} ${msg}`);
344
+ }
345
+ function error(msg) {
346
+ console.error(`${red("\u2717")} ${msg}`);
347
+ }
348
+ function warning(msg) {
349
+ console.log(`${yellow("!")} ${msg}`);
350
+ }
351
+ function truncate(str, maxLen) {
352
+ if (str.length <= maxLen) return str;
353
+ return str.slice(0, maxLen - 1) + "\u2026";
354
+ }
355
+ function formatNumber(n) {
356
+ return n.toLocaleString("en-US");
357
+ }
358
+ function table(headers, rows) {
359
+ const widths = headers.map((h, i) => {
360
+ const maxRow = rows.reduce((max, row) => Math.max(max, (row[i] || "").length), 0);
361
+ return Math.max(h.length, maxRow);
362
+ });
363
+ const headerLine = headers.map((h, i) => bold(h.padEnd(widths[i]))).join(" ");
364
+ const separator = widths.map((w) => "\u2500".repeat(w)).join(" ");
365
+ console.log(headerLine);
366
+ console.log(dim(separator));
367
+ for (const row of rows) {
368
+ const line = row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
369
+ console.log(line);
370
+ }
371
+ }
372
+ function progressBar(value, max, width = 20) {
373
+ const ratio = max > 0 ? value / max : 0;
374
+ const filled = Math.round(ratio * width);
375
+ return "\u2588".repeat(filled) + dim("\u2591".repeat(width - filled));
376
+ }
377
+
378
+ // src/cli/clipboard.ts
379
+ import { execSync } from "child_process";
380
+ function copyToClipboard(text) {
381
+ try {
382
+ const platform = process.platform;
383
+ let cmd;
384
+ if (platform === "darwin") {
385
+ cmd = "pbcopy";
386
+ } else if (platform === "win32") {
387
+ cmd = "clip.exe";
388
+ } else {
389
+ cmd = tryLinuxClipboard(text);
390
+ return cmd !== "";
391
+ }
392
+ execSync(cmd, {
393
+ input: text,
394
+ stdio: ["pipe", "ignore", "ignore"],
395
+ timeout: 3e3
396
+ });
397
+ return true;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+ function tryLinuxClipboard(text) {
403
+ const commands = [
404
+ "xclip -selection clipboard",
405
+ "xsel --clipboard --input",
406
+ "wl-copy"
407
+ ];
408
+ for (const cmd of commands) {
409
+ try {
410
+ execSync(cmd, {
411
+ input: text,
412
+ stdio: ["pipe", "ignore", "ignore"],
413
+ timeout: 3e3
414
+ });
415
+ return cmd;
416
+ } catch {
417
+ continue;
418
+ }
419
+ }
420
+ return "";
421
+ }
422
+
423
+ // src/cli/commands/shorten.ts
424
+ async function shortenCommand(client, url, flags) {
425
+ const result = await client.shorten(url);
426
+ success(bold(green(result.shortUrl)));
427
+ if (flags["no-copy"] !== true) {
428
+ const copied = copyToClipboard(result.shortUrl);
429
+ if (copied) {
430
+ console.log(` ${dim("Copied to clipboard!")}`);
431
+ }
432
+ }
433
+ }
434
+
435
+ // src/cli/commands/list.ts
436
+ async function listCommand(client, flags) {
437
+ const page = flags.page ? parseInt(String(flags.page), 10) : void 0;
438
+ const limit = flags.limit ? parseInt(String(flags.limit), 10) : void 0;
439
+ const result = await client.list({ page, limit });
440
+ if (result.urls.length === 0) {
441
+ warning("No URLs found. Shorten one with: shorter <url>");
442
+ return;
443
+ }
444
+ const headers = ["SHORT URL", "ORIGINAL", "CLICKS", "CREATED"];
445
+ const rows = result.urls.map((u) => [
446
+ u.shortUrl,
447
+ truncate(u.originalUrl, 35),
448
+ formatNumber(u.clickCount).padStart(6),
449
+ u.createdAt.split("T")[0]
450
+ ]);
451
+ table(headers, rows);
452
+ const { pagination, totalClicks } = result;
453
+ console.log(
454
+ `
455
+ ${dim(`Page ${pagination.page}/${pagination.totalPages} \xB7 ${formatNumber(pagination.total)} URLs \xB7 ${formatNumber(totalClicks)} total clicks`)}`
456
+ );
457
+ }
458
+
459
+ // src/cli/commands/delete.ts
460
+ async function deleteCommand(client, shortCode, flags) {
461
+ if (!/^[a-zA-Z0-9]{6}$/.test(shortCode)) {
462
+ error("Invalid short code. Must be 6 alphanumeric characters.");
463
+ process.exit(1);
464
+ }
465
+ if (flags.yes !== true) {
466
+ const ok = await confirm(`Delete ${bold(shortCode)}?`);
467
+ if (!ok) {
468
+ console.log(dim("Cancelled."));
469
+ return;
470
+ }
471
+ }
472
+ await client.delete(shortCode);
473
+ success(`Deleted ${bold(shortCode)}`);
474
+ }
475
+
476
+ // src/cli/commands/analytics.ts
477
+ function formatChange(current, previous) {
478
+ if (previous === 0) return "";
479
+ const pct = (current - previous) / previous * 100;
480
+ const sign = pct >= 0 ? "+" : "";
481
+ const color = pct >= 0 ? green : red;
482
+ return color(` (${sign}${pct.toFixed(1)}%)`);
483
+ }
484
+ function printBreakdown(title, items, maxItems = 5) {
485
+ if (!items || items.length === 0) return;
486
+ console.log(`
487
+ ${bold(title)}`);
488
+ const topItems = items.slice(0, maxItems);
489
+ const maxClicks = topItems[0]?.clicks || 1;
490
+ for (const item of topItems) {
491
+ const bar = progressBar(item.clicks, maxClicks);
492
+ console.log(
493
+ ` ${(item.value || "Unknown").padEnd(8)}${formatNumber(item.clicks).padStart(8)} ${item.percentage.toFixed(1).padStart(5)}% ${bar}`
494
+ );
495
+ }
496
+ }
497
+ async function analyticsCommand(client, shortCode, flags) {
498
+ const start = flags.start ? String(flags.start) : void 0;
499
+ const end = flags.end ? String(flags.end) : void 0;
500
+ const dimension = flags.dimension ? String(flags.dimension) : void 0;
501
+ if (!shortCode) {
502
+ const data = await client.analytics.overview({ start, end });
503
+ console.log(bold("\nAnalytics Overview") + dim(" (last 30 days)\n"));
504
+ console.log(` Total Clicks: ${bold(formatNumber(data.totalClicks))}${formatChange(data.totalClicks, data.prevPeriodClicks)}`);
505
+ if (data.uniqueVisitors !== null) {
506
+ console.log(` Unique Visitors: ${bold(formatNumber(data.uniqueVisitors))}${formatChange(data.uniqueVisitors, data.prevPeriodUnique ?? 0)}`);
507
+ }
508
+ if (data.topUrls.length > 0) {
509
+ console.log(`
510
+ ${bold("Top URLs")}`);
511
+ for (const url of data.topUrls.slice(0, 5)) {
512
+ console.log(` ${url.shortUrl.padEnd(30)} ${formatNumber(url.clicks).padStart(6)} clicks ${dim(truncate(url.originalUrl, 40))}`);
513
+ }
514
+ }
515
+ printBreakdown("Countries", data.countryBreakdown);
516
+ printBreakdown("Devices", data.deviceBreakdown);
517
+ printBreakdown("Browsers", data.browserBreakdown);
518
+ console.log();
519
+ } else {
520
+ const opts = { start, end };
521
+ if (dimension) {
522
+ opts.dimension = dimension;
523
+ }
524
+ const data = await client.analytics.url(shortCode, opts);
525
+ console.log(bold(`
526
+ Analytics for ${shortCode}
527
+ `));
528
+ console.log(` Total Clicks: ${bold(formatNumber(data.summary.totalClicks))}${formatChange(data.summary.totalClicks, data.summary.prevPeriodClicks)}`);
529
+ if (data.summary.uniqueVisitors !== null) {
530
+ console.log(` Unique Visitors: ${bold(formatNumber(data.summary.uniqueVisitors))}${formatChange(data.summary.uniqueVisitors, data.summary.prevPeriodUnique ?? 0)}`);
531
+ }
532
+ if (data.summary.topCountry) console.log(` Top Country: ${data.summary.topCountry}`);
533
+ if (data.summary.topReferrer) console.log(` Top Referrer: ${data.summary.topReferrer}`);
534
+ if (data.summary.topDevice) console.log(` Top Device: ${data.summary.topDevice}`);
535
+ if ("breakdown" in data && data.breakdown) {
536
+ printBreakdown(data.breakdown.dimension, data.breakdown.data);
537
+ }
538
+ console.log();
539
+ }
540
+ }
541
+
542
+ // src/cli/commands/config.ts
543
+ function configCommand(positional) {
544
+ const action = positional[0];
545
+ if (action === "path") {
546
+ console.log(getConfigPath());
547
+ return;
548
+ }
549
+ if (action === "get") {
550
+ const key = positional[1];
551
+ const config = loadConfig();
552
+ if (key === "api-key") {
553
+ if (config.apiKey) {
554
+ const masked = config.apiKey.slice(0, 5) + "\u2022\u2022\u2022\u2022" + config.apiKey.slice(-4);
555
+ console.log(masked);
556
+ } else {
557
+ warning("No API key configured.");
558
+ }
559
+ } else if (key === "base-url") {
560
+ console.log(config.baseUrl || "https://shorter.sh (default)");
561
+ } else {
562
+ error(`Unknown config key: ${key}. Valid keys: api-key, base-url`);
563
+ process.exit(1);
564
+ }
565
+ return;
566
+ }
567
+ if (action === "set") {
568
+ const key = positional[1];
569
+ const value = positional[2];
570
+ if (!value) {
571
+ error(`Missing value for ${key}.`);
572
+ process.exit(1);
573
+ }
574
+ const config = loadConfig();
575
+ if (key === "api-key") {
576
+ if (!value.startsWith("sk_")) {
577
+ error('Invalid API key format. Keys must start with "sk_".');
578
+ process.exit(1);
579
+ }
580
+ config.apiKey = value;
581
+ saveConfig(config);
582
+ success("API key saved.");
583
+ } else if (key === "base-url") {
584
+ try {
585
+ new URL(value);
586
+ } catch {
587
+ error("Invalid URL.");
588
+ process.exit(1);
589
+ }
590
+ config.baseUrl = value;
591
+ saveConfig(config);
592
+ success(`Base URL set to ${value}`);
593
+ } else {
594
+ error(`Unknown config key: ${key}. Valid keys: api-key, base-url`);
595
+ process.exit(1);
596
+ }
597
+ return;
598
+ }
599
+ error("Usage: shorter config <set|get|path> [key] [value]");
600
+ process.exit(1);
601
+ }
602
+
603
+ // src/cli/index.ts
604
+ var VERSION = "1.0.0";
605
+ function parseArgs(argv) {
606
+ const flags = {};
607
+ const positional = [];
608
+ for (let i = 0; i < argv.length; i++) {
609
+ const arg = argv[i];
610
+ if (arg.startsWith("--")) {
611
+ const key = arg.slice(2);
612
+ const next = argv[i + 1];
613
+ if (next && !next.startsWith("-")) {
614
+ flags[key] = next;
615
+ i++;
616
+ } else {
617
+ flags[key] = true;
618
+ }
619
+ } else if (arg.startsWith("-") && arg.length === 2) {
620
+ const key = arg.slice(1);
621
+ const next = argv[i + 1];
622
+ if (next && !next.startsWith("-")) {
623
+ flags[key] = next;
624
+ i++;
625
+ } else {
626
+ flags[key] = true;
627
+ }
628
+ } else {
629
+ positional.push(arg);
630
+ }
631
+ }
632
+ return { flags, positional };
633
+ }
634
+ function showHelp() {
635
+ console.log(`
636
+ ${bold("shorter.sh")} \u2014 URL shortener CLI
637
+
638
+ ${bold("Usage:")}
639
+ shorter <url> Shorten a URL
640
+ shorter list [--page N] [--limit N] List your URLs
641
+ shorter delete <shortCode> [--yes] Delete a URL
642
+ shorter analytics [shortCode] View analytics
643
+ shorter config <set|get|path> [...] Manage configuration
644
+
645
+ ${bold("Options:")}
646
+ --no-copy Don't copy to clipboard
647
+ --start <date> Analytics start date
648
+ --end <date> Analytics end date
649
+ --dimension <dim> Analytics breakdown dimension
650
+ --yes Skip confirmation prompts
651
+ -h, --help Show help
652
+ -v, --version Show version
653
+
654
+ ${bold("Aliases:")}
655
+ ls \u2192 list, rm \u2192 delete, stats \u2192 analytics
656
+
657
+ ${bold("Examples:")}
658
+ shorter https://example.com
659
+ shorter list --limit 10
660
+ shorter analytics xK9mP2 --dimension country
661
+ shorter config set api-key sk_your_key_here
662
+ `);
663
+ }
664
+ async function ensureApiKey() {
665
+ let apiKey = resolveApiKey();
666
+ if (apiKey) return apiKey;
667
+ console.log(bold("\nWelcome to shorter.sh!\n"));
668
+ console.log("To get started, you need an API key.");
669
+ console.log(`Get one at: ${cyan("https://shorter.sh/dashboard")}
670
+ `);
671
+ const input = await prompt("Enter your API key: ");
672
+ apiKey = input ?? void 0;
673
+ if (!apiKey) {
674
+ error("API key is required. Set it with: shorter config set api-key <key>");
675
+ process.exit(1);
676
+ }
677
+ if (!apiKey.startsWith("sk_")) {
678
+ error('Invalid API key format. Keys must start with "sk_".');
679
+ process.exit(1);
680
+ }
681
+ const config = loadConfig();
682
+ config.apiKey = apiKey;
683
+ saveConfig(config);
684
+ success("API key saved!\n");
685
+ return apiKey;
686
+ }
687
+ async function main() {
688
+ const { flags, positional } = parseArgs(process.argv.slice(2));
689
+ if (flags.h === true || flags.help === true || positional[0] === "help") {
690
+ showHelp();
691
+ return;
692
+ }
693
+ if (flags.v === true || flags.version === true) {
694
+ console.log(VERSION);
695
+ return;
696
+ }
697
+ if (positional[0] === "config") {
698
+ configCommand(positional.slice(1));
699
+ return;
700
+ }
701
+ let command;
702
+ let commandArgs;
703
+ const first = positional[0];
704
+ if (!first) {
705
+ showHelp();
706
+ return;
707
+ }
708
+ if (first.startsWith("http://") || first.startsWith("https://")) {
709
+ command = "shorten";
710
+ commandArgs = [first];
711
+ } else {
712
+ const aliases = { ls: "list", rm: "delete", stats: "analytics" };
713
+ command = aliases[first] || first;
714
+ commandArgs = positional.slice(1);
715
+ }
716
+ const apiKey = await ensureApiKey();
717
+ const client = new ShorterClient({ apiKey, baseUrl: resolveBaseUrl() });
718
+ switch (command) {
719
+ case "shorten":
720
+ if (!commandArgs[0]) {
721
+ error("URL is required. Usage: shorter <url>");
722
+ process.exit(1);
723
+ }
724
+ await shortenCommand(client, commandArgs[0], flags);
725
+ break;
726
+ case "list":
727
+ await listCommand(client, flags);
728
+ break;
729
+ case "delete":
730
+ if (!commandArgs[0]) {
731
+ error("Short code is required. Usage: shorter delete <shortCode>");
732
+ process.exit(1);
733
+ }
734
+ await deleteCommand(client, commandArgs[0], flags);
735
+ break;
736
+ case "analytics":
737
+ await analyticsCommand(client, commandArgs[0], flags);
738
+ break;
739
+ default:
740
+ error(`Unknown command: ${command}`);
741
+ showHelp();
742
+ process.exit(1);
743
+ }
744
+ }
745
+ main().catch((err) => {
746
+ if (err instanceof ShorterError) {
747
+ error(err.message);
748
+ if (err.code) {
749
+ console.error(dim(` Code: ${err.code}`));
750
+ }
751
+ } else {
752
+ error(err instanceof Error ? err.message : "An unexpected error occurred.");
753
+ }
754
+ process.exit(1);
755
+ });