@shorten-dev/cli 0.1.0 β†’ 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 CHANGED
@@ -6,9 +6,69 @@ import { Command } from "commander";
6
6
  // ../shared/src/constants.ts
7
7
  var MAX_TAGS_PER_LINK = 3;
8
8
  var MAX_TAG_LENGTH = 50;
9
+ var PRIVATE_IP_PATTERNS = [
10
+ /^127\./,
11
+ // loopback
12
+ /^10\./,
13
+ // class A private
14
+ /^172\.(1[6-9]|2\d|3[01])\./,
15
+ // class B private
16
+ /^192\.168\./,
17
+ // class C private
18
+ /^0\./,
19
+ // "this" network
20
+ /^0\.0\.0\.0$/,
21
+ /^169\.254\./,
22
+ // link-local
23
+ /^::1$/,
24
+ // IPv6 loopback
25
+ /^\[::1\]$/
26
+ ];
27
+ var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
28
+ "localhost",
29
+ "example.com",
30
+ "example.org",
31
+ "example.net",
32
+ "test.com",
33
+ "test.org",
34
+ "invalid",
35
+ "local"
36
+ ]);
37
+ function validateDestinationUrl(url) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ } catch {
42
+ return "Invalid URL";
43
+ }
44
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
45
+ return "Only http and https URLs are allowed";
46
+ }
47
+ const hostname = parsed.hostname.toLowerCase();
48
+ if (!hostname.includes(".")) {
49
+ return "URL must contain a valid domain name";
50
+ }
51
+ if (hostname.length < 4) {
52
+ return "URL domain is too short";
53
+ }
54
+ for (const pattern of PRIVATE_IP_PATTERNS) {
55
+ if (pattern.test(hostname)) {
56
+ return "URLs pointing to private or internal addresses are not allowed";
57
+ }
58
+ }
59
+ if (BLOCKED_HOSTS.has(hostname)) {
60
+ return `"${hostname}" is not allowed as a destination`;
61
+ }
62
+ if (hostname === "shorten.dev" || hostname.endsWith(".shorten.dev")) {
63
+ return "Cannot shorten URLs that point to shorten.dev";
64
+ }
65
+ if (parsed.href.toLowerCase().includes("javascript:") || parsed.href.toLowerCase().includes("data:")) {
66
+ return "URL contains a disallowed scheme";
67
+ }
68
+ return null;
69
+ }
9
70
 
10
71
  // src/api/client.ts
11
- var BASE_URL = "https://shorten.dev/api/v1";
12
72
  var ShortenApiError = class extends Error {
13
73
  constructor(status, body) {
14
74
  super(body.message);
@@ -17,13 +77,23 @@ var ShortenApiError = class extends Error {
17
77
  this.name = "ShortenApiError";
18
78
  }
19
79
  };
80
+ var NetworkError = class extends Error {
81
+ constructor(cause, url) {
82
+ super(`Network error: ${cause.message}`);
83
+ this.cause = cause;
84
+ this.url = url;
85
+ this.name = "NetworkError";
86
+ }
87
+ };
20
88
  var ApiClient = class {
21
89
  apiKey;
22
- constructor(apiKey) {
90
+ baseUrl;
91
+ constructor(apiKey, baseUrl) {
23
92
  this.apiKey = apiKey;
93
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
24
94
  }
25
95
  async get(path, params) {
26
- const url = new URL(`${BASE_URL}${path}`);
96
+ const url = new URL(`${this.baseUrl}${path}`);
27
97
  if (params) {
28
98
  for (const [k, v] of Object.entries(params)) {
29
99
  if (v !== void 0 && v !== "") url.searchParams.set(k, v);
@@ -32,39 +102,39 @@ var ApiClient = class {
32
102
  return this.request(url, { method: "GET" });
33
103
  }
34
104
  async post(path, body) {
35
- return this.request(new URL(`${BASE_URL}${path}`), {
105
+ return this.request(new URL(`${this.baseUrl}${path}`), {
36
106
  method: "POST",
37
107
  headers: { "Content-Type": "application/json" },
38
108
  body: body ? JSON.stringify(body) : void 0
39
109
  });
40
110
  }
41
- async patch(path, body) {
42
- return this.request(new URL(`${BASE_URL}${path}`), {
43
- method: "PATCH",
44
- headers: { "Content-Type": "application/json" },
45
- body: JSON.stringify(body)
46
- });
47
- }
48
- async del(path) {
49
- const res = await fetch(new URL(`${BASE_URL}${path}`), {
50
- method: "DELETE",
51
- headers: { Authorization: `Bearer ${this.apiKey}` }
52
- });
53
- if (!res.ok) {
54
- const body = await res.json();
55
- throw new ShortenApiError(res.status, body);
56
- }
57
- }
58
111
  async request(url, init) {
59
- const res = await fetch(url, {
60
- ...init,
61
- headers: {
62
- ...init.headers ?? {},
63
- Authorization: `Bearer ${this.apiKey}`
64
- }
65
- });
112
+ let res;
113
+ try {
114
+ res = await fetch(url, {
115
+ ...init,
116
+ headers: {
117
+ ...init.headers ?? {},
118
+ Authorization: `Bearer ${this.apiKey}`
119
+ }
120
+ });
121
+ } catch (err) {
122
+ throw new NetworkError(
123
+ err instanceof Error ? err : new Error(String(err)),
124
+ url.toString()
125
+ );
126
+ }
66
127
  if (!res.ok) {
67
- const body = await res.json();
128
+ let body;
129
+ try {
130
+ body = await res.json();
131
+ } catch {
132
+ body = {
133
+ error: `HTTP ${res.status}`,
134
+ message: res.statusText || `Request failed with status ${res.status}`,
135
+ status: res.status
136
+ };
137
+ }
68
138
  throw new ShortenApiError(res.status, body);
69
139
  }
70
140
  return await res.json();
@@ -72,9 +142,10 @@ var ApiClient = class {
72
142
  };
73
143
 
74
144
  // src/config.ts
75
- import { readFileSync } from "fs";
145
+ import { readFileSync, writeFileSync } from "fs";
76
146
  import { join } from "path";
77
147
  import { homedir } from "os";
148
+ var DEFAULT_API_URL = "https://shorten.dev/api/v1";
78
149
  var CONFIG_PATH = join(homedir(), ".shorten.json");
79
150
  function loadConfig() {
80
151
  try {
@@ -84,26 +155,31 @@ function loadConfig() {
84
155
  return {};
85
156
  }
86
157
  }
158
+ function saveConfig(config) {
159
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
160
+ }
87
161
  function resolveApiKey(flagKey) {
88
162
  const key = flagKey ?? process.env["SHORTEN_API_KEY"] ?? loadConfig().api_key;
89
163
  if (!key) {
90
164
  console.error(
91
- "No API key found. Set SHORTEN_API_KEY or pass --key.\nGenerate one at https://shorten.dev/api-keys"
165
+ 'No API key found. Set SHORTEN_API_KEY or pass --key.\nRun "shorten login" or visit https://shorten.dev/app/api-keys'
92
166
  );
93
167
  process.exit(1);
94
168
  }
95
169
  return key;
96
170
  }
171
+ function resolveApiUrl(flagUrl) {
172
+ return flagUrl ?? process.env["SHORTEN_API_URL"] ?? loadConfig().api_url ?? DEFAULT_API_URL;
173
+ }
97
174
 
98
- // src/utils/clipboard.ts
99
- async function copyToClipboard(text) {
100
- try {
101
- const { default: clipboardy } = await import("clipboardy");
102
- await clipboardy.write(text);
103
- return true;
104
- } catch {
105
- return false;
106
- }
175
+ // src/utils/client-factory.ts
176
+ function resolveClient(cmd) {
177
+ const globals = cmd.optsWithGlobals();
178
+ const config = loadConfig();
179
+ const apiKey = resolveApiKey(globals["key"]);
180
+ const apiUrl = resolveApiUrl(globals["apiUrl"]);
181
+ const client = new ApiClient(apiKey, apiUrl);
182
+ return { client, config, apiKey };
107
183
  }
108
184
 
109
185
  // src/utils/output.ts
@@ -151,18 +227,71 @@ function formatDate(iso) {
151
227
  });
152
228
  }
153
229
 
230
+ // src/utils/errors.ts
231
+ function handleCommandError(err) {
232
+ if (err instanceof ShortenApiError) {
233
+ switch (err.status) {
234
+ case 401:
235
+ error("Invalid API key. Check your key or generate a new one.");
236
+ info(' Run "shorten login" or visit https://shorten.dev/app/api-keys');
237
+ break;
238
+ case 403:
239
+ error("Forbidden \u2014 your API key doesn't have the required scope.");
240
+ break;
241
+ case 404:
242
+ error(err.message || "Not found.");
243
+ break;
244
+ case 429:
245
+ error("Rate limit exceeded. Try again later.");
246
+ break;
247
+ default:
248
+ error(err.message);
249
+ }
250
+ process.exit(1);
251
+ }
252
+ if (err instanceof NetworkError) {
253
+ error(`Could not connect to the API (${err.url}).`);
254
+ info(" Check your internet connection or use --api-url to set the correct endpoint.");
255
+ process.exit(1);
256
+ }
257
+ const message = err instanceof Error ? err.message : String(err);
258
+ error(`Unexpected error: ${message}`);
259
+ process.exit(1);
260
+ }
261
+
262
+ // src/utils/clipboard.ts
263
+ async function copyToClipboard(text) {
264
+ try {
265
+ const { default: clipboardy } = await import("clipboardy");
266
+ await clipboardy.write(text);
267
+ return true;
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
154
273
  // src/commands/shorten.ts
274
+ function normalizeUrl(raw) {
275
+ if (!/^https?:\/\//i.test(raw)) {
276
+ return `https://${raw}`;
277
+ }
278
+ return raw;
279
+ }
155
280
  function registerShortenCommand(program2) {
156
- const cmd = program2.command("create", { isDefault: true, hidden: true }).argument("<url>", "URL to shorten").option("-s, --slug <slug>", "Custom slug").option("-t, --tag <tag>", "Add tags (repeatable)", (val, acc) => [...acc, val], []).option("-q, --quiet", "Output only the short URL").option("-j, --json", "Output as JSON").option("--no-copy", "Don't copy to clipboard").action(async (url, opts) => {
157
- const config = loadConfig();
158
- const globalKey = cmd.optsWithGlobals()["key"];
159
- const apiKey = resolveApiKey(globalKey);
160
- const client = new ApiClient(apiKey);
281
+ const cmd = program2.command("create", { isDefault: true, hidden: true }).argument("<url>", "URL to shorten").option("-s, --slug <slug>", "Custom slug").option("-t, --tag <tag>", "Add tags (repeatable)", (val, acc) => [...acc, val], []).option("-q, --quiet", "Output only the short URL").option("-j, --json", "Output as JSON").option("--no-copy", "Don't copy to clipboard").action(async (rawUrl, opts) => {
282
+ const { client, config } = resolveClient(cmd);
161
283
  const format = resolveFormat({
162
284
  json: opts.json,
163
285
  quiet: opts.quiet,
164
286
  configDefault: config.default_format
165
287
  });
288
+ const url = normalizeUrl(rawUrl);
289
+ const validationError = validateDestinationUrl(url);
290
+ if (validationError) {
291
+ error(validationError);
292
+ process.exit(1);
293
+ return;
294
+ }
166
295
  const tags = opts.tag;
167
296
  if (tags && tags.length > MAX_TAGS_PER_LINK) {
168
297
  error(`Too many tags: got ${tags.length}, max is ${MAX_TAGS_PER_LINK}`);
@@ -201,12 +330,7 @@ function registerShortenCommand(program2) {
201
330
  info(` tags: ${res.link.tags.join(", ")}`);
202
331
  }
203
332
  } catch (err) {
204
- if (err instanceof ShortenApiError) {
205
- error(err.message);
206
- process.exit(1);
207
- return;
208
- }
209
- throw err;
333
+ handleCommandError(err);
210
334
  }
211
335
  });
212
336
  }
@@ -217,9 +341,7 @@ function registerListCommand(program2) {
217
341
  "--sort <field>",
218
342
  "Sort by created_at or slug"
219
343
  ).option("--order <dir>", "asc or desc (default: desc)").option("-j, --json", "Output as JSON").action(async (opts) => {
220
- const globalKey = cmd.optsWithGlobals()["key"];
221
- const apiKey = resolveApiKey(globalKey);
222
- const client = new ApiClient(apiKey);
344
+ const { client } = resolveClient(cmd);
223
345
  const params = {};
224
346
  if (opts.limit) params["limit"] = opts.limit;
225
347
  if (opts.status) params["status"] = opts.status;
@@ -253,12 +375,7 @@ function registerListCommand(program2) {
253
375
  Showing ${res.data.length} of ${res.total} links (page ${res.page}/${res.total_pages})`
254
376
  );
255
377
  } catch (err) {
256
- if (err instanceof ShortenApiError) {
257
- error(err.message);
258
- process.exit(1);
259
- return;
260
- }
261
- throw err;
378
+ handleCommandError(err);
262
379
  }
263
380
  });
264
381
  }
@@ -272,11 +389,9 @@ import pc2 from "picocolors";
272
389
  function registerStatsCommand(program2) {
273
390
  const cmd = program2.command("stats <slug>").description("View click analytics for a link").option(
274
391
  "-p, --period <period>",
275
- "Time window: 7d, 30d, 90d, or all (default: 7d)"
392
+ "Time window: 7d, 30d, or 90d (default: 7d)"
276
393
  ).option("-j, --json", "Output as JSON").action(async (slug, opts) => {
277
- const globalKey = cmd.optsWithGlobals()["key"];
278
- const apiKey = resolveApiKey(globalKey);
279
- const client = new ApiClient(apiKey);
394
+ const { client } = resolveClient(cmd);
280
395
  const params = {};
281
396
  if (opts.period) params["period"] = opts.period;
282
397
  try {
@@ -288,7 +403,7 @@ function registerStatsCommand(program2) {
288
403
  json(res);
289
404
  return;
290
405
  }
291
- const period = res.period === "all" ? "all time" : res.period;
406
+ const period = res.period;
292
407
  console.log(
293
408
  `
294
409
  ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_clicks))} clicks (${period})`
@@ -307,11 +422,10 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
307
422
  console.log(
308
423
  pc2.bold(" Top countries".padEnd(colWidth)) + pc2.bold("Top referrers".padEnd(colWidth)) + pc2.bold("Devices")
309
424
  );
310
- const deviceEntries = [
311
- ["Desktop", res.devices.desktop],
312
- ["Mobile", res.devices.mobile],
313
- ["Tablet", res.devices.tablet]
314
- ];
425
+ const deviceEntries = res.top_devices.slice(0, maxRows).map((d) => [
426
+ d.device.charAt(0).toUpperCase() + d.device.slice(1),
427
+ d.count
428
+ ]);
315
429
  for (let i = 0; i < maxRows; i++) {
316
430
  let line = " ";
317
431
  const country = countries[i];
@@ -319,7 +433,7 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
319
433
  const pct = Math.round(
320
434
  country.count / totalForPct * 100
321
435
  );
322
- line += `${country.country_code.padEnd(6)}${String(pct).padStart(3)}%`.padEnd(
436
+ line += `${country.country.padEnd(6)}${String(pct).padStart(3)}%`.padEnd(
323
437
  colWidth
324
438
  );
325
439
  } else {
@@ -347,12 +461,7 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
347
461
  }
348
462
  console.log();
349
463
  } catch (err) {
350
- if (err instanceof ShortenApiError) {
351
- error(err.message);
352
- process.exit(1);
353
- return;
354
- }
355
- throw err;
464
+ handleCommandError(err);
356
465
  }
357
466
  });
358
467
  }
@@ -361,9 +470,7 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
361
470
  import pc3 from "picocolors";
362
471
  function registerWhoamiCommand(program2) {
363
472
  const cmd = program2.command("whoami").description("Display authenticated user and API key info").option("-j, --json", "Output as JSON").action(async (opts) => {
364
- const globalKey = cmd.optsWithGlobals()["key"];
365
- const apiKey = resolveApiKey(globalKey);
366
- const client = new ApiClient(apiKey);
473
+ const { client, apiKey } = resolveClient(cmd);
367
474
  try {
368
475
  const usage = await client.get("/usage");
369
476
  if (opts.json) {
@@ -383,12 +490,7 @@ function registerWhoamiCommand(program2) {
383
490
  ` Resets: ${formatDate(usage.reset_at)}`
384
491
  );
385
492
  } catch (err) {
386
- if (err instanceof ShortenApiError) {
387
- error(err.message);
388
- process.exit(1);
389
- return;
390
- }
391
- throw err;
493
+ handleCommandError(err);
392
494
  }
393
495
  });
394
496
  }
@@ -397,14 +499,143 @@ function maskKey(key) {
397
499
  return key.slice(0, 3) + "\u2026" + key.slice(-4);
398
500
  }
399
501
 
502
+ // src/commands/login.ts
503
+ import { createInterface } from "readline";
504
+ function registerLoginCommand(program2) {
505
+ const cmd = program2.command("login").description("Authenticate with your API key").action(async () => {
506
+ const globals = cmd.optsWithGlobals();
507
+ const apiUrl = resolveApiUrl(globals["apiUrl"]);
508
+ const appUrl = apiUrl.replace(/\/api\/v1\/?$/, "/app/api-keys");
509
+ if (!process.stdin.isTTY) {
510
+ error(
511
+ "Non-interactive terminal detected. Use --key flag instead:\n shorten --key sk_your_key whoami"
512
+ );
513
+ process.exit(1);
514
+ return;
515
+ }
516
+ console.log(`
517
+ Open your browser to get an API key:
518
+ ${appUrl}
519
+ `);
520
+ try {
521
+ const { exec } = await import("child_process");
522
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
523
+ exec(`${openCmd} ${appUrl}`);
524
+ } catch {
525
+ }
526
+ const rl = createInterface({
527
+ input: process.stdin,
528
+ output: process.stdout
529
+ });
530
+ const key = await new Promise((resolve) => {
531
+ rl.question("Paste your API key: ", (answer) => {
532
+ rl.close();
533
+ resolve(answer.trim());
534
+ });
535
+ });
536
+ if (!key) {
537
+ error("No key provided.");
538
+ process.exit(1);
539
+ return;
540
+ }
541
+ if (!key.startsWith("sk_")) {
542
+ error('Invalid API key format. Keys start with "sk_".');
543
+ process.exit(1);
544
+ return;
545
+ }
546
+ try {
547
+ const client = new ApiClient(key, apiUrl);
548
+ await client.get("/usage");
549
+ const config = loadConfig();
550
+ config.api_key = key;
551
+ saveConfig(config);
552
+ success("Logged in successfully. API key saved to ~/.shorten.json");
553
+ } catch (err) {
554
+ error("Could not verify the API key.");
555
+ handleCommandError(err);
556
+ }
557
+ });
558
+ }
559
+
560
+ // src/commands/config.ts
561
+ import { unlinkSync } from "fs";
562
+ var VALID_KEYS = [
563
+ "api_key",
564
+ "api_url",
565
+ "default_format",
566
+ "copy_to_clipboard"
567
+ ];
568
+ function maskValue(key, value) {
569
+ if (key === "api_key" && typeof value === "string" && value.length > 8) {
570
+ return value.slice(0, 3) + "\u2026" + value.slice(-4);
571
+ }
572
+ return String(value);
573
+ }
574
+ function registerConfigCommand(program2) {
575
+ const configCmd = program2.command("config").description("Manage CLI configuration");
576
+ configCmd.command("list").description("Show all configuration values").action(() => {
577
+ const config = loadConfig();
578
+ const entries = Object.entries(config);
579
+ if (entries.length === 0) {
580
+ info(`No configuration found. Config file: ${CONFIG_PATH}`);
581
+ return;
582
+ }
583
+ for (const [key, value] of entries) {
584
+ console.log(`${key} = ${maskValue(key, value)}`);
585
+ }
586
+ });
587
+ configCmd.command("get <key>").description("Get a configuration value").action((key) => {
588
+ if (!VALID_KEYS.includes(key)) {
589
+ error(`Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
590
+ process.exit(1);
591
+ return;
592
+ }
593
+ const config = loadConfig();
594
+ const value = config[key];
595
+ if (value === void 0) {
596
+ info(`"${key}" is not set.`);
597
+ return;
598
+ }
599
+ console.log(maskValue(key, value));
600
+ });
601
+ configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
602
+ if (!VALID_KEYS.includes(key)) {
603
+ error(`Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
604
+ process.exit(1);
605
+ return;
606
+ }
607
+ const config = loadConfig();
608
+ if (key === "copy_to_clipboard") {
609
+ config[key] = value === "true";
610
+ } else {
611
+ config[key] = value;
612
+ }
613
+ saveConfig(config);
614
+ success(`Set ${key} = ${maskValue(key, value)}`);
615
+ });
616
+ configCmd.command("reset").description("Delete the configuration file").action(() => {
617
+ try {
618
+ unlinkSync(CONFIG_PATH);
619
+ success(`Deleted ${CONFIG_PATH}`);
620
+ } catch {
621
+ info("No configuration file to delete.");
622
+ }
623
+ });
624
+ configCmd.command("path").description("Print the configuration file path").action(() => {
625
+ console.log(CONFIG_PATH);
626
+ });
627
+ }
628
+
400
629
  // src/cli.ts
401
630
  function createProgram() {
402
631
  const program2 = new Command();
403
- program2.name("shorten").description("Shorten URLs from your terminal").version("0.1.0").option("-k, --key <key>", "API key (overrides SHORTEN_API_KEY)").option("--no-color", "Disable colored output").configureOutput({ writeErr: (str) => process.stderr.write(str) });
632
+ program2.name("shorten").description("Shorten URLs from your terminal").version("0.1.1").option("-k, --key <key>", "API key (overrides SHORTEN_API_KEY)").option("--api-url <url>", "API base URL (overrides SHORTEN_API_URL)").option("--no-color", "Disable colored output").configureOutput({ writeErr: (str) => process.stderr.write(str) });
404
633
  registerShortenCommand(program2);
405
634
  registerListCommand(program2);
406
635
  registerStatsCommand(program2);
407
636
  registerWhoamiCommand(program2);
637
+ registerLoginCommand(program2);
638
+ registerConfigCommand(program2);
408
639
  return program2;
409
640
  }
410
641
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../../shared/src/constants.ts","../src/api/client.ts","../src/config.ts","../src/utils/clipboard.ts","../src/utils/output.ts","../src/commands/shorten.ts","../src/commands/list.ts","../src/commands/stats.ts","../src/commands/whoami.ts","../src/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { registerShortenCommand } from \"./commands/shorten.js\";\nimport { registerListCommand } from \"./commands/list.js\";\nimport { registerStatsCommand } from \"./commands/stats.js\";\nimport { registerWhoamiCommand } from \"./commands/whoami.js\";\n\nexport function createProgram(): Command {\n const program = new Command();\n\n program\n .name(\"shorten\")\n .description(\"Shorten URLs from your terminal\")\n .version(\"0.1.0\")\n .option(\"-k, --key <key>\", \"API key (overrides SHORTEN_API_KEY)\")\n .option(\"--no-color\", \"Disable colored output\")\n .configureOutput({ writeErr: (str) => process.stderr.write(str) });\n\n registerShortenCommand(program);\n registerListCommand(program);\n registerStatsCommand(program);\n registerWhoamiCommand(program);\n\n return program;\n}\n","export const SLUG_LENGTH = 7;\nexport const BASE_URL = \"https://shorten.dev\";\n\nexport const MAX_TAGS_PER_LINK = 3;\nexport const MAX_TAG_LENGTH = 50;\n\nexport const RATE_LIMIT = {\n requests_per_hour: 300,\n} as const;\n\nexport const LINK_STATUSES = [\"active\", \"flagged\"] as const;\n\nexport const API_KEY_SCOPES = [\"read\", \"write\", \"admin\"] as const;\n\nexport const ANALYTICS_PERIODS = [\"7d\", \"30d\", \"90d\", \"all\"] as const;\n\n\nexport const TIMEZONES = [\n \"America/New_York\",\n \"America/Chicago\",\n \"America/Denver\",\n \"America/Los_Angeles\",\n \"America/Anchorage\",\n \"Pacific/Honolulu\",\n \"Europe/London\",\n \"Europe/Paris\",\n \"Europe/Berlin\",\n \"Asia/Tokyo\",\n \"Asia/Shanghai\",\n \"Asia/Kolkata\",\n \"Australia/Sydney\",\n \"UTC\",\n] as const;\n","import type { ApiError } from \"@shorten/shared\";\n\nconst BASE_URL = \"https://shorten.dev/api/v1\";\n\nexport class ShortenApiError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: ApiError,\n ) {\n super(body.message);\n this.name = \"ShortenApiError\";\n }\n}\n\nexport class ApiClient {\n private apiKey: string;\n\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n async get<T>(path: string, params?: Record<string, string>): Promise<T> {\n const url = new URL(`${BASE_URL}${path}`);\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined && v !== \"\") url.searchParams.set(k, v);\n }\n }\n return this.request(url, { method: \"GET\" });\n }\n\n async post<T>(path: string, body?: unknown): Promise<T> {\n return this.request(new URL(`${BASE_URL}${path}`), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: body ? JSON.stringify(body) : undefined,\n });\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n return this.request(new URL(`${BASE_URL}${path}`), {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n }\n\n async del(path: string): Promise<void> {\n const res = await fetch(new URL(`${BASE_URL}${path}`), {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${this.apiKey}` },\n });\n if (!res.ok) {\n const body = (await res.json()) as ApiError;\n throw new ShortenApiError(res.status, body);\n }\n }\n\n private async request<T>(url: URL, init: RequestInit): Promise<T> {\n const res = await fetch(url, {\n ...init,\n headers: {\n ...((init.headers as Record<string, string>) ?? {}),\n Authorization: `Bearer ${this.apiKey}`,\n },\n });\n\n if (!res.ok) {\n const body = (await res.json()) as ApiError;\n throw new ShortenApiError(res.status, body);\n }\n\n return (await res.json()) as T;\n }\n}\n","import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nexport interface ShortenConfig {\n api_key?: string;\n default_format?: \"pretty\" | \"short\" | \"json\";\n copy_to_clipboard?: boolean;\n}\n\nconst CONFIG_PATH = join(homedir(), \".shorten.json\");\n\nexport function loadConfig(): ShortenConfig {\n try {\n const raw = readFileSync(CONFIG_PATH, \"utf-8\");\n return JSON.parse(raw) as ShortenConfig;\n } catch {\n return {};\n }\n}\n\nexport function resolveApiKey(flagKey?: string): string {\n const key = flagKey ?? process.env[\"SHORTEN_API_KEY\"] ?? loadConfig().api_key;\n\n if (!key) {\n console.error(\n \"No API key found. Set SHORTEN_API_KEY or pass --key.\\n\" +\n \"Generate one at https://shorten.dev/api-keys\",\n );\n process.exit(1);\n }\n\n return key;\n}\n","export async function copyToClipboard(text: string): Promise<boolean> {\n try {\n const { default: clipboardy } = await import(\"clipboardy\");\n await clipboardy.write(text);\n return true;\n } catch {\n return false;\n }\n}\n","import pc from \"picocolors\";\n\nexport type OutputFormat = \"pretty\" | \"json\" | \"quiet\";\n\nexport function resolveFormat(opts: {\n json?: boolean;\n quiet?: boolean;\n configDefault?: string;\n}): OutputFormat {\n if (opts.json) return \"json\";\n if (opts.quiet) return \"quiet\";\n if (opts.configDefault === \"json\") return \"json\";\n if (opts.configDefault === \"short\") return \"quiet\";\n return \"pretty\";\n}\n\nexport function success(msg: string): void {\n console.log(pc.green(\"βœ“\") + \" \" + msg);\n}\n\nexport function error(msg: string): void {\n console.error(pc.red(\"βœ—\") + \" \" + msg);\n}\n\nexport function info(msg: string): void {\n console.log(pc.dim(msg));\n}\n\nexport function json(data: unknown): void {\n console.log(JSON.stringify(data, null, 2));\n}\n\nexport function table(\n headers: string[],\n rows: string[][],\n columnWidths?: number[],\n): void {\n const widths =\n columnWidths ??\n headers.map((h, i) =>\n Math.max(h.length, ...rows.map((r) => (r[i] ?? \"\").length)),\n );\n\n const header = headers\n .map((h, i) => h.padEnd(widths[i] ?? h.length))\n .join(\" \");\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n\n for (const row of rows) {\n console.log(\n row.map((c, i) => c.padEnd(widths[i] ?? c.length)).join(\" \"),\n );\n }\n}\n\nexport function formatNumber(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\nexport function formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\nexport function percentBar(pct: number, width = 10): string {\n const filled = Math.round((pct / 100) * width);\n return pc.cyan(\"β–ˆ\".repeat(filled)) + pc.dim(\"β–‘\".repeat(width - filled));\n}\n","import type { Command } from \"commander\";\nimport { MAX_TAGS_PER_LINK, MAX_TAG_LENGTH } from \"@shorten/shared\";\nimport type { CreateLinkRequest, CreateLinkResponse } from \"@shorten/shared\";\nimport { ApiClient, ShortenApiError } from \"../api/client.js\";\nimport { loadConfig, resolveApiKey } from \"../config.js\";\nimport { copyToClipboard } from \"../utils/clipboard.js\";\nimport * as out from \"../utils/output.js\";\n\ninterface ShortenOptions {\n slug?: string;\n tag?: string[];\n quiet?: boolean;\n json?: boolean;\n copy?: boolean;\n}\n\nexport function registerShortenCommand(program: Command): void {\n const cmd = program\n .command(\"create\", { isDefault: true, hidden: true })\n .argument(\"<url>\", \"URL to shorten\")\n .option(\"-s, --slug <slug>\", \"Custom slug\")\n .option(\"-t, --tag <tag>\", \"Add tags (repeatable)\", (val: string, acc: string[]) => [...acc, val], [] as string[])\n .option(\"-q, --quiet\", \"Output only the short URL\")\n .option(\"-j, --json\", \"Output as JSON\")\n .option(\"--no-copy\", \"Don't copy to clipboard\")\n .action(async (url: string, opts: ShortenOptions) => {\n const config = loadConfig();\n const globalKey = cmd.optsWithGlobals()[\"key\"] as string | undefined;\n const apiKey = resolveApiKey(globalKey);\n const client = new ApiClient(apiKey);\n const format = out.resolveFormat({\n json: opts.json,\n quiet: opts.quiet,\n configDefault: config.default_format,\n });\n\n const tags = opts.tag;\n if (tags && tags.length > MAX_TAGS_PER_LINK) {\n out.error(`Too many tags: got ${tags.length}, max is ${MAX_TAGS_PER_LINK}`);\n process.exit(1);\n return;\n }\n const longTag = tags?.find((t) => t.length > MAX_TAG_LENGTH);\n if (longTag) {\n out.error(`Tag \"${longTag}\" exceeds ${MAX_TAG_LENGTH} characters`);\n process.exit(1);\n return;\n }\n\n const body: CreateLinkRequest = {\n destination_url: url,\n custom_slug: opts.slug,\n tags,\n };\n\n try {\n const res = await client.post<CreateLinkResponse>(\"/links\", body);\n\n if (format === \"json\") {\n out.json(res);\n return;\n }\n\n if (format === \"quiet\") {\n console.log(res.short_url);\n return;\n }\n\n const shouldCopy =\n opts.copy !== false && (config.copy_to_clipboard ?? true);\n\n let suffix = \"\";\n if (shouldCopy) {\n const copied = await copyToClipboard(res.short_url);\n if (copied) suffix = \" (copied to clipboard)\";\n }\n\n out.success(`${res.short_url}${suffix}`);\n\n if (res.link.tags.length > 0) {\n out.info(` tags: ${res.link.tags.join(\", \")}`);\n }\n } catch (err) {\n if (err instanceof ShortenApiError) {\n out.error(err.message);\n process.exit(1);\n return;\n }\n throw err;\n }\n });\n}\n","import type { Command } from \"commander\";\nimport type { Link, PaginatedResponse } from \"@shorten/shared\";\nimport { ApiClient, ShortenApiError } from \"../api/client.js\";\nimport { resolveApiKey } from \"../config.js\";\nimport * as out from \"../utils/output.js\";\n\ninterface ListOptions {\n limit?: string;\n status?: string;\n search?: string;\n sort?: string;\n order?: string;\n json?: boolean;\n}\n\nexport function registerListCommand(program: Command): void {\n const cmd = program\n .command(\"list\")\n .description(\"List your links\")\n .option(\"-n, --limit <n>\", \"Number of results (default: 10, max: 100)\")\n .option(\"--status <status>\", \"Filter by active or flagged\")\n .option(\"--search <query>\", \"Search in slug, URL, and tags\")\n .option(\n \"--sort <field>\",\n \"Sort by created_at or slug\",\n )\n .option(\"--order <dir>\", \"asc or desc (default: desc)\")\n .option(\"-j, --json\", \"Output as JSON\")\n .action(async (opts: ListOptions) => {\n const globalKey = cmd.optsWithGlobals()[\"key\"] as string | undefined;\n const apiKey = resolveApiKey(globalKey);\n const client = new ApiClient(apiKey);\n\n const params: Record<string, string> = {};\n if (opts.limit) params[\"limit\"] = opts.limit;\n if (opts.status) params[\"status\"] = opts.status;\n if (opts.search) params[\"search\"] = opts.search;\n if (opts.sort) params[\"sort\"] = opts.sort;\n if (opts.order) params[\"order\"] = opts.order;\n\n try {\n const res = await client.get<PaginatedResponse<Link>>(\n \"/links\",\n params,\n );\n\n if (opts.json) {\n out.json(res);\n return;\n }\n\n if (res.data.length === 0) {\n out.info(\"No links found.\");\n return;\n }\n\n out.table(\n [\"Slug\", \"Destination\", \"Status\", \"Created\"],\n res.data.map((link) => [\n link.slug,\n truncate(link.destination_url, 40),\n link.status,\n out.formatDate(link.created_at),\n ]),\n );\n\n out.info(\n `\\nShowing ${res.data.length} of ${res.total} links (page ${res.page}/${res.total_pages})`,\n );\n } catch (err) {\n if (err instanceof ShortenApiError) {\n out.error(err.message);\n process.exit(1);\n return;\n }\n throw err;\n }\n });\n}\n\nfunction truncate(str: string, maxLen: number): string {\n if (str.length <= maxLen) return str;\n return str.slice(0, maxLen - 1) + \"…\";\n}\n","import type { Command } from \"commander\";\nimport type { AnalyticsResponse } from \"@shorten/shared\";\nimport { ApiClient, ShortenApiError } from \"../api/client.js\";\nimport { resolveApiKey } from \"../config.js\";\nimport * as out from \"../utils/output.js\";\nimport pc from \"picocolors\";\n\ninterface StatsOptions {\n period?: string;\n json?: boolean;\n}\n\nexport function registerStatsCommand(program: Command): void {\n const cmd = program\n .command(\"stats <slug>\")\n .description(\"View click analytics for a link\")\n .option(\n \"-p, --period <period>\",\n \"Time window: 7d, 30d, 90d, or all (default: 7d)\",\n )\n .option(\"-j, --json\", \"Output as JSON\")\n .action(async (slug: string, opts: StatsOptions) => {\n const globalKey = cmd.optsWithGlobals()[\"key\"] as string | undefined;\n const apiKey = resolveApiKey(globalKey);\n const client = new ApiClient(apiKey);\n\n const params: Record<string, string> = {};\n if (opts.period) params[\"period\"] = opts.period;\n\n try {\n const res = await client.get<AnalyticsResponse>(\n `/links/${slug}/analytics`,\n params,\n );\n\n if (opts.json) {\n out.json(res);\n return;\n }\n\n const period = res.period === \"all\" ? \"all time\" : res.period;\n console.log(\n `\\n${pc.bold(`shorten.dev/${res.slug}`)} β€” ${pc.cyan(out.formatNumber(res.total_clicks))} clicks (${period})`,\n );\n console.log(\n pc.dim(\n ` ${out.formatNumber(res.unique_visitors)} unique visitors`,\n ),\n );\n\n console.log();\n\n const colWidth = 24;\n const maxRows = 5;\n\n // Build columns\n const countries = res.top_countries.slice(0, maxRows);\n const referrers = res.top_referrers.slice(0, maxRows);\n\n const totalForPct = res.total_clicks || 1;\n\n // Header\n console.log(\n pc.bold(\" Top countries\".padEnd(colWidth)) +\n pc.bold(\"Top referrers\".padEnd(colWidth)) +\n pc.bold(\"Devices\"),\n );\n\n // Rows\n const deviceEntries = [\n [\"Desktop\", res.devices.desktop],\n [\"Mobile\", res.devices.mobile],\n [\"Tablet\", res.devices.tablet],\n ] as const;\n\n for (let i = 0; i < maxRows; i++) {\n let line = \" \";\n\n // Country column\n const country = countries[i];\n if (country) {\n const pct = Math.round(\n (country.count / totalForPct) * 100,\n );\n line += `${country.country_code.padEnd(6)}${String(pct).padStart(3)}%`.padEnd(\n colWidth,\n );\n } else {\n line += \"\".padEnd(colWidth);\n }\n\n // Referrer column\n const referrer = referrers[i];\n if (referrer) {\n const pct = Math.round(\n (referrer.count / totalForPct) * 100,\n );\n line += `${referrer.referrer.padEnd(16)}${String(pct).padStart(3)}%`.padEnd(\n colWidth,\n );\n } else {\n line += \"\".padEnd(colWidth);\n }\n\n // Device column\n const device = deviceEntries[i];\n if (device) {\n const pct = Math.round(\n (device[1] / totalForPct) * 100,\n );\n line += `${device[0].padEnd(10)}${String(pct).padStart(3)}%`;\n }\n\n console.log(line);\n }\n\n console.log();\n } catch (err) {\n if (err instanceof ShortenApiError) {\n out.error(err.message);\n process.exit(1);\n return;\n }\n throw err;\n }\n });\n}\n","import type { Command } from \"commander\";\nimport type { UsageResponse } from \"@shorten/shared\";\nimport { ApiClient, ShortenApiError } from \"../api/client.js\";\nimport { resolveApiKey } from \"../config.js\";\nimport * as out from \"../utils/output.js\";\nimport pc from \"picocolors\";\n\ninterface WhoamiOptions {\n json?: boolean;\n}\n\nexport function registerWhoamiCommand(program: Command): void {\n const cmd = program\n .command(\"whoami\")\n .description(\"Display authenticated user and API key info\")\n .option(\"-j, --json\", \"Output as JSON\")\n .action(async (opts: WhoamiOptions) => {\n const globalKey = cmd.optsWithGlobals()[\"key\"] as string | undefined;\n const apiKey = resolveApiKey(globalKey);\n const client = new ApiClient(apiKey);\n\n try {\n const usage = await client.get<UsageResponse>(\"/usage\");\n\n if (opts.json) {\n out.json({\n key_prefix: maskKey(apiKey),\n rate_limit: usage,\n });\n return;\n }\n\n console.log(\n ` Key: ${pc.cyan(maskKey(apiKey))}`,\n );\n console.log(\n ` Rate limit: ${pc.bold(String(usage.remaining))}/${usage.limit} remaining`,\n );\n console.log(\n ` Resets: ${out.formatDate(usage.reset_at)}`,\n );\n } catch (err) {\n if (err instanceof ShortenApiError) {\n out.error(err.message);\n process.exit(1);\n return;\n }\n throw err;\n }\n });\n}\n\nfunction maskKey(key: string): string {\n if (key.length <= 8) return key;\n return key.slice(0, 3) + \"…\" + key.slice(-4);\n}\n","import { createProgram } from \"./cli.js\";\n\nconst program = createProgram();\nprogram.parseAsync(process.argv);\n"],"mappings":"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","names":["program","program","pc","program","pc","program","program"]}
1
+ {"version":3,"sources":["../src/cli.ts","../../shared/src/constants.ts","../src/api/client.ts","../src/config.ts","../src/utils/client-factory.ts","../src/utils/output.ts","../src/utils/errors.ts","../src/utils/clipboard.ts","../src/commands/shorten.ts","../src/commands/list.ts","../src/commands/stats.ts","../src/commands/whoami.ts","../src/commands/login.ts","../src/commands/config.ts","../src/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { registerShortenCommand } from \"./commands/shorten.js\";\nimport { registerListCommand } from \"./commands/list.js\";\nimport { registerStatsCommand } from \"./commands/stats.js\";\nimport { registerWhoamiCommand } from \"./commands/whoami.js\";\nimport { registerLoginCommand } from \"./commands/login.js\";\nimport { registerConfigCommand } from \"./commands/config.js\";\n\ndeclare const __CLI_VERSION__: string;\n\nexport function createProgram(): Command {\n const program = new Command();\n\n program\n .name(\"shorten\")\n .description(\"Shorten URLs from your terminal\")\n .version(__CLI_VERSION__)\n .option(\"-k, --key <key>\", \"API key (overrides SHORTEN_API_KEY)\")\n .option(\"--api-url <url>\", \"API base URL (overrides SHORTEN_API_URL)\")\n .option(\"--no-color\", \"Disable colored output\")\n .configureOutput({ writeErr: (str) => process.stderr.write(str) });\n\n registerShortenCommand(program);\n registerListCommand(program);\n registerStatsCommand(program);\n registerWhoamiCommand(program);\n registerLoginCommand(program);\n registerConfigCommand(program);\n\n return program;\n}\n","export const SLUG_LENGTH = 7;\nexport const SLUG_MIN_LENGTH = 3;\nexport const SLUG_MAX_LENGTH = 50;\nexport const SLUG_PATTERN = /^[a-zA-Z0-9_-]+$/;\nexport const BASE_URL = \"https://shorten.dev\";\n\nexport const MAX_TAGS_PER_LINK = 3;\nexport const MAX_TAG_LENGTH = 50;\n\n/**\n * Reserved slugs that cannot be used as custom short links.\n * Covers: app routes, common subdomains, platform terms, admin/system paths,\n * well-known paths, social/SEO, legal, and potentially confusing terms.\n *\n * All entries are lowercase β€” check with `.toLowerCase()` before comparing.\n */\nexport const RESERVED_SLUGS = new Set([\n // ── App routes & pages ──────────────────────────────────────────────\n \"app\",\n \"api\",\n \"auth\",\n \"login\",\n \"logout\",\n \"signup\",\n \"register\",\n \"signin\",\n \"signout\",\n \"sign-in\",\n \"sign-up\",\n \"sign-out\",\n \"log-in\",\n \"log-out\",\n \"callback\",\n \"dashboard\",\n \"settings\",\n \"account\",\n \"profile\",\n \"billing\",\n \"upgrade\",\n \"pricing\",\n \"plans\",\n \"onboarding\",\n \"welcome\",\n \"verify\",\n \"confirm\",\n \"reset\",\n \"password\",\n \"forgot\",\n \"forgot-password\",\n \"reset-password\",\n \"change-password\",\n \"invite\",\n \"join\",\n\n // ── Public pages ────────────────────────────────────────────────────\n \"about\",\n \"about-us\",\n \"donate\",\n \"docs\",\n \"documentation\",\n \"support\",\n \"help\",\n \"contact\",\n \"contact-us\",\n \"faq\",\n \"blog\",\n \"changelog\",\n \"updates\",\n \"news\",\n \"press\",\n \"careers\",\n \"jobs\",\n \"team\",\n \"partners\",\n \"affiliates\",\n \"referrals\",\n\n // ── Legal ───────────────────────────────────────────────────────────\n \"terms\",\n \"terms-of-service\",\n \"tos\",\n \"privacy\",\n \"privacy-policy\",\n \"cookies\",\n \"cookie-policy\",\n \"gdpr\",\n \"dmca\",\n \"legal\",\n \"compliance\",\n \"acceptable-use\",\n \"aup\",\n\n // ── Admin & system ──────────────────────────────────────────────────\n \"admin\",\n \"administrator\",\n \"root\",\n \"system\",\n \"sys\",\n \"internal\",\n \"debug\",\n \"dev\",\n \"staging\",\n \"test\",\n \"sandbox\",\n \"demo\",\n \"console\",\n \"panel\",\n \"manage\",\n \"manager\",\n \"config\",\n \"configuration\",\n \"setup\",\n \"install\",\n \"cron\",\n \"worker\",\n \"workers\",\n \"queue\",\n \"health\",\n \"healthcheck\",\n \"health-check\",\n \"status\",\n \"uptime\",\n \"metrics\",\n \"monitor\",\n \"monitoring\",\n \"logs\",\n \"trace\",\n\n // ── API & developer ─────────────────────────────────────────────────\n \"api-keys\",\n \"apikeys\",\n \"api-key\",\n \"tokens\",\n \"token\",\n \"oauth\",\n \"oauth2\",\n \"openid\",\n \"sso\",\n \"saml\",\n \"webhooks\",\n \"webhook\",\n \"graphql\",\n \"rest\",\n \"sdk\",\n \"cli\",\n \"developer\",\n \"developers\",\n \"playground\",\n \"explorer\",\n \"schema\",\n \"swagger\",\n \"openapi\",\n \"redoc\",\n\n // ── Analytics & data ────────────────────────────────────────────────\n \"analytics\",\n \"stats\",\n \"statistics\",\n \"reports\",\n \"report\",\n \"insights\",\n \"events\",\n \"clicks\",\n \"links\",\n \"link\",\n \"urls\",\n \"url\",\n \"domains\",\n \"domain\",\n\n // ── User & account ──────────────────────────────────────────────────\n \"user\",\n \"users\",\n \"me\",\n \"my\",\n \"you\",\n \"self\",\n \"notifications\",\n \"inbox\",\n \"messages\",\n \"mail\",\n \"email\",\n \"preferences\",\n \"subscription\",\n \"subscriptions\",\n\n // ── Social & SEO ────────────────────────────────────────────────────\n \"share\",\n \"embed\",\n \"widget\",\n \"follow\",\n \"like\",\n \"star\",\n \"bookmark\",\n \"feed\",\n \"rss\",\n \"atom\",\n \"sitemap\",\n \"robots\",\n \"manifest\",\n \"humans\",\n \"ads\",\n \"sponsors\",\n\n // ── Well-known paths ────────────────────────────────────────────────\n \"favicon\",\n \"wp-admin\",\n \"wp-login\",\n \"wp-content\",\n \"wordpress\",\n \"xmlrpc\",\n \"cgi-bin\",\n \"phpmyadmin\",\n \"env\",\n \"git\",\n \"svn\",\n \"htaccess\",\n \"htpasswd\",\n \"ssh\",\n \"ftp\",\n \"cpanel\",\n \"webmail\",\n \"autodiscover\",\n \"well-known\",\n \"_next\",\n\n // ── Common subdomains (if ever used as slugs) ───────────────────────\n \"www\",\n \"mail\",\n \"ftp\",\n \"cdn\",\n \"assets\",\n \"static\",\n \"media\",\n \"images\",\n \"img\",\n \"files\",\n \"download\",\n \"downloads\",\n \"upload\",\n \"uploads\",\n \"storage\",\n\n // ── Brand protection ────────────────────────────────────────────────\n \"shorten\",\n \"shorten-dev\",\n \"shortendev\",\n \"official\",\n \"verified\",\n\n // ── Potentially confusing / misleading ──────────────────────────────\n \"null\",\n \"undefined\",\n \"nil\",\n \"none\",\n \"true\",\n \"false\",\n \"nan\",\n \"infinity\",\n \"error\",\n \"404\",\n \"500\",\n \"403\",\n \"401\",\n \"new\",\n \"create\",\n \"edit\",\n \"delete\",\n \"remove\",\n \"update\",\n \"search\",\n \"home\",\n \"index\",\n \"default\",\n \"public\",\n \"private\",\n \"example\",\n \"test\",\n \"temp\",\n \"tmp\",\n\n // ── Payment & commerce ──────────────────────────────────────────────\n \"checkout\",\n \"payment\",\n \"payments\",\n \"pay\",\n \"invoice\",\n \"invoices\",\n \"receipt\",\n \"refund\",\n \"stripe\",\n \"paypal\",\n\n // ── Security-sensitive ──────────────────────────────────────────────\n \"security\",\n \"vulnerability\",\n \"report\",\n \"abuse\",\n \"spam\",\n \"phishing\",\n \"malware\",\n \"block\",\n \"blocked\",\n \"ban\",\n \"banned\",\n \"flag\",\n \"flagged\",\n \"suspend\",\n \"suspended\",\n \"deactivated\",\n \"disabled\",\n]);\n\n/**\n * Brand names that cannot appear anywhere inside a custom slug.\n * Checked via substring match (case-insensitive) to catch phishing patterns\n * like \"paypal-login\", \"apple-verify\", \"google-security-alert\", etc.\n *\n * All entries are lowercase β€” normalize with `.toLowerCase()` before checking.\n */\nexport const BRANDED_SLUG_TERMS = [\n // ── Finance & payments ──────────────────────────────────────────────\n \"paypal\",\n \"venmo\",\n \"cashapp\",\n \"cash-app\",\n \"zelle\",\n \"stripe\",\n \"square\",\n \"wise\",\n \"revolut\",\n \"coinbase\",\n \"binance\",\n \"kraken\",\n \"robinhood\",\n \"blockchain\",\n \"metamask\",\n \"opensea\",\n \"ledger\",\n\n // ── Banks ───────────────────────────────────────────────────────────\n \"chase\",\n \"wellsfargo\",\n \"wells-fargo\",\n \"bankofamerica\",\n \"bank-of-america\",\n \"citibank\",\n \"hsbc\",\n \"barclays\",\n \"capitalone\",\n \"capital-one\",\n \"usbank\",\n \"us-bank\",\n \"pnc\",\n \"truist\",\n \"schwab\",\n \"fidelity\",\n \"vanguard\",\n \"amex\",\n \"mastercard\",\n \"visa\",\n\n // ── Big tech ────────────────────────────────────────────────────────\n \"google\",\n \"gmail\",\n \"youtube\",\n \"apple\",\n \"icloud\",\n \"itunes\",\n \"microsoft\",\n \"outlook\",\n \"hotmail\",\n \"windows\",\n \"xbox\",\n \"linkedin\",\n \"amazon\",\n \"aws\",\n \"facebook\",\n \"instagram\",\n \"whatsapp\",\n \"messenger\",\n \"meta\",\n \"tiktok\",\n \"snapchat\",\n \"twitter\",\n \"x-com\",\n \"netflix\",\n \"spotify\",\n \"discord\",\n \"telegram\",\n \"signal\",\n \"slack\",\n \"zoom\",\n \"dropbox\",\n \"github\",\n \"gitlab\",\n \"bitbucket\",\n\n // ── E-commerce & delivery ───────────────────────────────────────────\n \"ebay\",\n \"walmart\",\n \"target\",\n \"bestbuy\",\n \"best-buy\",\n \"costco\",\n \"shopify\",\n \"etsy\",\n \"aliexpress\",\n \"alibaba\",\n \"fedex\",\n \"ups\",\n \"usps\",\n \"dhl\",\n\n // ── Telecom & ISP ──────────────────────────────────────────────────\n \"verizon\",\n \"att\",\n \"t-mobile\",\n \"tmobile\",\n \"comcast\",\n \"xfinity\",\n \"spectrum\",\n\n // ── Government & institutions ───────────────────────────────────────\n \"irs\",\n \"ssa\",\n \"medicare\",\n \"medicaid\",\n \"dmv\",\n \"usps\",\n \"fbi\",\n \"cia\",\n \"nsa\",\n \"dhs\",\n \"sec\",\n\n // ── Streaming & gaming ──────────────────────────────────────────────\n \"hulu\",\n \"disney\",\n \"hbomax\",\n \"hbo-max\",\n \"peacock\",\n \"paramount\",\n \"twitch\",\n \"steam\",\n \"playstation\",\n \"nintendo\",\n \"epicgames\",\n \"epic-games\",\n \"roblox\",\n \"fortnite\",\n\n // ── Security & identity ─────────────────────────────────────────────\n \"norton\",\n \"mcafee\",\n \"kaspersky\",\n \"lastpass\",\n \"onepassword\",\n \"1password\",\n \"okta\",\n \"auth0\",\n \"docusign\",\n\n // ── Travel & rideshare ──────────────────────────────────────────────\n \"uber\",\n \"lyft\",\n \"airbnb\",\n \"booking\",\n \"expedia\",\n \"delta\",\n \"united\",\n \"southwest\",\n \"american-airlines\",\n\n // ── Common phishing action words (combined with brand = high signal) ─\n // These are only blocked as part of the substring check, not alone.\n // e.g., \"verify\" alone is fine as a reserved slug; \"apple-verify\" is blocked.\n] as const;\n\n/**\n * Check if a slug contains a protected brand term.\n * Returns the matched brand term, or null if clean.\n */\nexport function findBrandTermInSlug(slug: string): string | null {\n const lower = slug.toLowerCase();\n for (const term of BRANDED_SLUG_TERMS) {\n if (lower.includes(term)) return term;\n }\n return null;\n}\n\n// ── Destination URL validation ─────────────────────────────────────────\n\n/** Private/reserved IP ranges that should never be redirect targets. */\nconst PRIVATE_IP_PATTERNS = [\n /^127\\./, // loopback\n /^10\\./, // class A private\n /^172\\.(1[6-9]|2\\d|3[01])\\./, // class B private\n /^192\\.168\\./, // class C private\n /^0\\./, // \"this\" network\n /^0\\.0\\.0\\.0$/,\n /^169\\.254\\./, // link-local\n /^::1$/, // IPv6 loopback\n /^\\[::1\\]$/,\n];\n\n/** Hosts that are non-routable or reserved for documentation. */\nconst BLOCKED_HOSTS = new Set([\n \"localhost\",\n \"example.com\",\n \"example.org\",\n \"example.net\",\n \"test.com\",\n \"test.org\",\n \"invalid\",\n \"local\",\n]);\n\n/**\n * Validate a destination URL for safety.\n * Expects a fully-formed URL (with protocol).\n * Returns an error message string, or null if valid.\n */\nexport function validateDestinationUrl(url: string): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return \"Invalid URL\";\n }\n\n // Must be http or https\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return \"Only http and https URLs are allowed\";\n }\n\n const hostname = parsed.hostname.toLowerCase();\n\n // Must contain a dot (blocks \"localhost\", single-label hosts, etc.)\n if (!hostname.includes(\".\")) {\n return \"URL must contain a valid domain name\";\n }\n\n // Minimum hostname length: \"x.xx\" = 4 chars\n if (hostname.length < 4) {\n return \"URL domain is too short\";\n }\n\n // Block private/internal IPs\n for (const pattern of PRIVATE_IP_PATTERNS) {\n if (pattern.test(hostname)) {\n return \"URLs pointing to private or internal addresses are not allowed\";\n }\n }\n\n // Block non-routable / reserved hosts\n if (BLOCKED_HOSTS.has(hostname)) {\n return `\"${hostname}\" is not allowed as a destination`;\n }\n\n // Block self-referencing URLs (redirect loops)\n if (hostname === \"shorten.dev\" || hostname.endsWith(\".shorten.dev\")) {\n return \"Cannot shorten URLs that point to shorten.dev\";\n }\n\n // Block data: and javascript: in the path (defense-in-depth, URL constructor\n // would already reject these but worth being explicit)\n if (parsed.href.toLowerCase().includes(\"javascript:\") || parsed.href.toLowerCase().includes(\"data:\")) {\n return \"URL contains a disallowed scheme\";\n }\n\n return null;\n}\n\nexport const RATE_LIMIT = {\n requests_per_hour: 300,\n} as const;\n\nexport const LINK_STATUSES = [\"active\", \"flagged\"] as const;\n\nexport const API_KEY_SCOPES = [\"read\", \"write\", \"admin\"] as const;\n\nexport const ANALYTICS_PERIODS = [\"7d\", \"30d\", \"90d\"] as const;\n\n\nexport const TIMEZONES = [\n \"America/New_York\",\n \"America/Chicago\",\n \"America/Denver\",\n \"America/Los_Angeles\",\n \"America/Anchorage\",\n \"Pacific/Honolulu\",\n \"Europe/London\",\n \"Europe/Paris\",\n \"Europe/Berlin\",\n \"Asia/Tokyo\",\n \"Asia/Shanghai\",\n \"Asia/Kolkata\",\n \"Australia/Sydney\",\n \"UTC\",\n] as const;\n","import type { ApiError } from \"@shorten/shared\";\n\nexport class ShortenApiError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: ApiError,\n ) {\n super(body.message);\n this.name = \"ShortenApiError\";\n }\n}\n\nexport class NetworkError extends Error {\n constructor(\n public readonly cause: Error,\n public readonly url: string,\n ) {\n super(`Network error: ${cause.message}`);\n this.name = \"NetworkError\";\n }\n}\n\nexport class ApiClient {\n private apiKey: string;\n private baseUrl: string;\n\n constructor(apiKey: string, baseUrl: string) {\n this.apiKey = apiKey;\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n }\n\n async get<T>(path: string, params?: Record<string, string>): Promise<T> {\n const url = new URL(`${this.baseUrl}${path}`);\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined && v !== \"\") url.searchParams.set(k, v);\n }\n }\n return this.request(url, { method: \"GET\" });\n }\n\n async post<T>(path: string, body?: unknown): Promise<T> {\n return this.request(new URL(`${this.baseUrl}${path}`), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: body ? JSON.stringify(body) : undefined,\n });\n }\n\n private async request<T>(url: URL, init: RequestInit): Promise<T> {\n let res: Response;\n try {\n res = await fetch(url, {\n ...init,\n headers: {\n ...((init.headers as Record<string, string>) ?? {}),\n Authorization: `Bearer ${this.apiKey}`,\n },\n });\n } catch (err) {\n throw new NetworkError(\n err instanceof Error ? err : new Error(String(err)),\n url.toString(),\n );\n }\n\n if (!res.ok) {\n let body: ApiError;\n try {\n body = (await res.json()) as ApiError;\n } catch {\n body = {\n error: `HTTP ${res.status}`,\n message: res.statusText || `Request failed with status ${res.status}`,\n status: res.status,\n };\n }\n throw new ShortenApiError(res.status, body);\n }\n\n return (await res.json()) as T;\n }\n}\n","import { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nexport interface ShortenConfig {\n api_key?: string;\n api_url?: string;\n default_format?: \"pretty\" | \"short\" | \"json\";\n copy_to_clipboard?: boolean;\n}\n\nexport const DEFAULT_API_URL = \"https://shorten.dev/api/v1\";\n\nexport const CONFIG_PATH = join(homedir(), \".shorten.json\");\n\nexport function loadConfig(): ShortenConfig {\n try {\n const raw = readFileSync(CONFIG_PATH, \"utf-8\");\n return JSON.parse(raw) as ShortenConfig;\n } catch {\n return {};\n }\n}\n\nexport function saveConfig(config: ShortenConfig): void {\n writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\nexport function resolveApiKey(flagKey?: string): string {\n const key = flagKey ?? process.env[\"SHORTEN_API_KEY\"] ?? loadConfig().api_key;\n\n if (!key) {\n console.error(\n \"No API key found. Set SHORTEN_API_KEY or pass --key.\\n\" +\n 'Run \"shorten login\" or visit https://shorten.dev/app/api-keys',\n );\n process.exit(1);\n }\n\n return key;\n}\n\nexport function resolveApiUrl(flagUrl?: string): string {\n return (\n flagUrl ??\n process.env[\"SHORTEN_API_URL\"] ??\n loadConfig().api_url ??\n DEFAULT_API_URL\n );\n}\n","import type { Command } from \"commander\";\nimport { ApiClient } from \"../api/client.js\";\nimport { loadConfig, resolveApiKey, resolveApiUrl } from \"../config.js\";\nimport type { ShortenConfig } from \"../config.js\";\n\nexport interface ResolvedClient {\n client: ApiClient;\n config: ShortenConfig;\n apiKey: string;\n}\n\nexport function resolveClient(cmd: Command): ResolvedClient {\n const globals = cmd.optsWithGlobals() as Record<string, string | undefined>;\n const config = loadConfig();\n const apiKey = resolveApiKey(globals[\"key\"]);\n const apiUrl = resolveApiUrl(globals[\"apiUrl\"]);\n const client = new ApiClient(apiKey, apiUrl);\n return { client, config, apiKey };\n}\n","import pc from \"picocolors\";\n\nexport type OutputFormat = \"pretty\" | \"json\" | \"quiet\";\n\nexport function resolveFormat(opts: {\n json?: boolean;\n quiet?: boolean;\n configDefault?: string;\n}): OutputFormat {\n if (opts.json) return \"json\";\n if (opts.quiet) return \"quiet\";\n if (opts.configDefault === \"json\") return \"json\";\n if (opts.configDefault === \"short\") return \"quiet\";\n return \"pretty\";\n}\n\nexport function success(msg: string): void {\n console.log(pc.green(\"βœ“\") + \" \" + msg);\n}\n\nexport function error(msg: string): void {\n console.error(pc.red(\"βœ—\") + \" \" + msg);\n}\n\nexport function info(msg: string): void {\n console.log(pc.dim(msg));\n}\n\nexport function json(data: unknown): void {\n console.log(JSON.stringify(data, null, 2));\n}\n\nexport function table(\n headers: string[],\n rows: string[][],\n columnWidths?: number[],\n): void {\n const widths =\n columnWidths ??\n headers.map((h, i) =>\n Math.max(h.length, ...rows.map((r) => (r[i] ?? \"\").length)),\n );\n\n const header = headers\n .map((h, i) => h.padEnd(widths[i] ?? h.length))\n .join(\" \");\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n\n for (const row of rows) {\n console.log(\n row.map((c, i) => c.padEnd(widths[i] ?? c.length)).join(\" \"),\n );\n }\n}\n\nexport function formatNumber(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\nexport function formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\nexport function percentBar(pct: number, width = 10): string {\n const filled = Math.round((pct / 100) * width);\n return pc.cyan(\"β–ˆ\".repeat(filled)) + pc.dim(\"β–‘\".repeat(width - filled));\n}\n","import { ShortenApiError, NetworkError } from \"../api/client.js\";\nimport * as out from \"./output.js\";\n\nexport function handleCommandError(err: unknown): never {\n if (err instanceof ShortenApiError) {\n switch (err.status) {\n case 401:\n out.error(\"Invalid API key. Check your key or generate a new one.\");\n out.info(' Run \"shorten login\" or visit https://shorten.dev/app/api-keys');\n break;\n case 403:\n out.error(\"Forbidden β€” your API key doesn't have the required scope.\");\n break;\n case 404:\n out.error(err.message || \"Not found.\");\n break;\n case 429:\n out.error(\"Rate limit exceeded. Try again later.\");\n break;\n default:\n out.error(err.message);\n }\n process.exit(1);\n }\n\n if (err instanceof NetworkError) {\n out.error(`Could not connect to the API (${err.url}).`);\n out.info(\" Check your internet connection or use --api-url to set the correct endpoint.\");\n process.exit(1);\n }\n\n const message = err instanceof Error ? err.message : String(err);\n out.error(`Unexpected error: ${message}`);\n process.exit(1);\n}\n","export async function copyToClipboard(text: string): Promise<boolean> {\n try {\n const { default: clipboardy } = await import(\"clipboardy\");\n await clipboardy.write(text);\n return true;\n } catch {\n return false;\n }\n}\n","import type { Command } from \"commander\";\nimport {\n MAX_TAGS_PER_LINK,\n MAX_TAG_LENGTH,\n validateDestinationUrl,\n} from \"@shorten/shared\";\nimport type { CreateLinkRequest, CreateLinkResponse } from \"@shorten/shared\";\nimport { resolveClient } from \"../utils/client-factory.js\";\nimport { handleCommandError } from \"../utils/errors.js\";\nimport { copyToClipboard } from \"../utils/clipboard.js\";\nimport * as out from \"../utils/output.js\";\n\ninterface ShortenOptions {\n slug?: string;\n tag?: string[];\n quiet?: boolean;\n json?: boolean;\n copy?: boolean;\n}\n\nfunction normalizeUrl(raw: string): string {\n if (!/^https?:\\/\\//i.test(raw)) {\n return `https://${raw}`;\n }\n return raw;\n}\n\nexport function registerShortenCommand(program: Command): void {\n const cmd = program\n .command(\"create\", { isDefault: true, hidden: true })\n .argument(\"<url>\", \"URL to shorten\")\n .option(\"-s, --slug <slug>\", \"Custom slug\")\n .option(\"-t, --tag <tag>\", \"Add tags (repeatable)\", (val: string, acc: string[]) => [...acc, val], [] as string[])\n .option(\"-q, --quiet\", \"Output only the short URL\")\n .option(\"-j, --json\", \"Output as JSON\")\n .option(\"--no-copy\", \"Don't copy to clipboard\")\n .action(async (rawUrl: string, opts: ShortenOptions) => {\n const { client, config } = resolveClient(cmd);\n const format = out.resolveFormat({\n json: opts.json,\n quiet: opts.quiet,\n configDefault: config.default_format,\n });\n\n // Normalize and validate URL client-side\n const url = normalizeUrl(rawUrl);\n const validationError = validateDestinationUrl(url);\n if (validationError) {\n out.error(validationError);\n process.exit(1);\n return;\n }\n\n const tags = opts.tag;\n if (tags && tags.length > MAX_TAGS_PER_LINK) {\n out.error(`Too many tags: got ${tags.length}, max is ${MAX_TAGS_PER_LINK}`);\n process.exit(1);\n return;\n }\n const longTag = tags?.find((t) => t.length > MAX_TAG_LENGTH);\n if (longTag) {\n out.error(`Tag \"${longTag}\" exceeds ${MAX_TAG_LENGTH} characters`);\n process.exit(1);\n return;\n }\n\n const body: CreateLinkRequest = {\n destination_url: url,\n custom_slug: opts.slug,\n tags,\n };\n\n try {\n const res = await client.post<CreateLinkResponse>(\"/links\", body);\n\n if (format === \"json\") {\n out.json(res);\n return;\n }\n\n if (format === \"quiet\") {\n console.log(res.short_url);\n return;\n }\n\n const shouldCopy =\n opts.copy !== false && (config.copy_to_clipboard ?? true);\n\n let suffix = \"\";\n if (shouldCopy) {\n const copied = await copyToClipboard(res.short_url);\n if (copied) suffix = \" (copied to clipboard)\";\n }\n\n out.success(`${res.short_url}${suffix}`);\n\n if (res.link.tags.length > 0) {\n out.info(` tags: ${res.link.tags.join(\", \")}`);\n }\n } catch (err) {\n handleCommandError(err);\n }\n });\n}\n","import type { Command } from \"commander\";\nimport type { Link, PaginatedResponse } from \"@shorten/shared\";\nimport { resolveClient } from \"../utils/client-factory.js\";\nimport { handleCommandError } from \"../utils/errors.js\";\nimport * as out from \"../utils/output.js\";\n\ninterface ListOptions {\n limit?: string;\n status?: string;\n search?: string;\n sort?: string;\n order?: string;\n json?: boolean;\n}\n\nexport function registerListCommand(program: Command): void {\n const cmd = program\n .command(\"list\")\n .description(\"List your links\")\n .option(\"-n, --limit <n>\", \"Number of results (default: 10, max: 100)\")\n .option(\"--status <status>\", \"Filter by active or flagged\")\n .option(\"--search <query>\", \"Search in slug, URL, and tags\")\n .option(\n \"--sort <field>\",\n \"Sort by created_at or slug\",\n )\n .option(\"--order <dir>\", \"asc or desc (default: desc)\")\n .option(\"-j, --json\", \"Output as JSON\")\n .action(async (opts: ListOptions) => {\n const { client } = resolveClient(cmd);\n\n const params: Record<string, string> = {};\n if (opts.limit) params[\"limit\"] = opts.limit;\n if (opts.status) params[\"status\"] = opts.status;\n if (opts.search) params[\"search\"] = opts.search;\n if (opts.sort) params[\"sort\"] = opts.sort;\n if (opts.order) params[\"order\"] = opts.order;\n\n try {\n const res = await client.get<PaginatedResponse<Link>>(\n \"/links\",\n params,\n );\n\n if (opts.json) {\n out.json(res);\n return;\n }\n\n if (res.data.length === 0) {\n out.info(\"No links found.\");\n return;\n }\n\n out.table(\n [\"Slug\", \"Destination\", \"Status\", \"Created\"],\n res.data.map((link) => [\n link.slug,\n truncate(link.destination_url, 40),\n link.status,\n out.formatDate(link.created_at),\n ]),\n );\n\n out.info(\n `\\nShowing ${res.data.length} of ${res.total} links (page ${res.page}/${res.total_pages})`,\n );\n } catch (err) {\n handleCommandError(err);\n }\n });\n}\n\nfunction truncate(str: string, maxLen: number): string {\n if (str.length <= maxLen) return str;\n return str.slice(0, maxLen - 1) + \"…\";\n}\n","import type { Command } from \"commander\";\nimport type { AnalyticsResponse, DeviceEntry } from \"@shorten/shared\";\nimport { resolveClient } from \"../utils/client-factory.js\";\nimport { handleCommandError } from \"../utils/errors.js\";\nimport * as out from \"../utils/output.js\";\nimport pc from \"picocolors\";\n\ninterface StatsOptions {\n period?: string;\n json?: boolean;\n}\n\nexport function registerStatsCommand(program: Command): void {\n const cmd = program\n .command(\"stats <slug>\")\n .description(\"View click analytics for a link\")\n .option(\n \"-p, --period <period>\",\n \"Time window: 7d, 30d, or 90d (default: 7d)\",\n )\n .option(\"-j, --json\", \"Output as JSON\")\n .action(async (slug: string, opts: StatsOptions) => {\n const { client } = resolveClient(cmd);\n\n const params: Record<string, string> = {};\n if (opts.period) params[\"period\"] = opts.period;\n\n try {\n const res = await client.get<AnalyticsResponse>(\n `/links/${slug}/analytics`,\n params,\n );\n\n if (opts.json) {\n out.json(res);\n return;\n }\n\n const period = res.period;\n console.log(\n `\\n${pc.bold(`shorten.dev/${res.slug}`)} β€” ${pc.cyan(out.formatNumber(res.total_clicks))} clicks (${period})`,\n );\n console.log(\n pc.dim(\n ` ${out.formatNumber(res.unique_visitors)} unique visitors`,\n ),\n );\n\n console.log();\n\n const colWidth = 24;\n const maxRows = 5;\n\n // Build columns\n const countries = res.top_countries.slice(0, maxRows);\n const referrers = res.top_referrers.slice(0, maxRows);\n\n const totalForPct = res.total_clicks || 1;\n\n // Header\n console.log(\n pc.bold(\" Top countries\".padEnd(colWidth)) +\n pc.bold(\"Top referrers\".padEnd(colWidth)) +\n pc.bold(\"Devices\"),\n );\n\n // Rows\n const deviceEntries = res.top_devices.slice(0, maxRows).map((d: DeviceEntry) => [\n d.device.charAt(0).toUpperCase() + d.device.slice(1),\n d.count,\n ] as const);\n\n for (let i = 0; i < maxRows; i++) {\n let line = \" \";\n\n // Country column\n const country = countries[i];\n if (country) {\n const pct = Math.round(\n (country.count / totalForPct) * 100,\n );\n line += `${country.country.padEnd(6)}${String(pct).padStart(3)}%`.padEnd(\n colWidth,\n );\n } else {\n line += \"\".padEnd(colWidth);\n }\n\n // Referrer column\n const referrer = referrers[i];\n if (referrer) {\n const pct = Math.round(\n (referrer.count / totalForPct) * 100,\n );\n line += `${referrer.referrer.padEnd(16)}${String(pct).padStart(3)}%`.padEnd(\n colWidth,\n );\n } else {\n line += \"\".padEnd(colWidth);\n }\n\n // Device column\n const device = deviceEntries[i];\n if (device) {\n const pct = Math.round(\n (device[1] / totalForPct) * 100,\n );\n line += `${device[0].padEnd(10)}${String(pct).padStart(3)}%`;\n }\n\n console.log(line);\n }\n\n console.log();\n } catch (err) {\n handleCommandError(err);\n }\n });\n}\n","import type { Command } from \"commander\";\nimport type { UsageResponse } from \"@shorten/shared\";\nimport { resolveClient } from \"../utils/client-factory.js\";\nimport { handleCommandError } from \"../utils/errors.js\";\nimport * as out from \"../utils/output.js\";\nimport pc from \"picocolors\";\n\ninterface WhoamiOptions {\n json?: boolean;\n}\n\nexport function registerWhoamiCommand(program: Command): void {\n const cmd = program\n .command(\"whoami\")\n .description(\"Display authenticated user and API key info\")\n .option(\"-j, --json\", \"Output as JSON\")\n .action(async (opts: WhoamiOptions) => {\n const { client, apiKey } = resolveClient(cmd);\n\n try {\n const usage = await client.get<UsageResponse>(\"/usage\");\n\n if (opts.json) {\n out.json({\n key_prefix: maskKey(apiKey),\n rate_limit: usage,\n });\n return;\n }\n\n console.log(\n ` Key: ${pc.cyan(maskKey(apiKey))}`,\n );\n console.log(\n ` Rate limit: ${pc.bold(String(usage.remaining))}/${usage.limit} remaining`,\n );\n console.log(\n ` Resets: ${out.formatDate(usage.reset_at)}`,\n );\n } catch (err) {\n handleCommandError(err);\n }\n });\n}\n\nfunction maskKey(key: string): string {\n if (key.length <= 8) return key;\n return key.slice(0, 3) + \"…\" + key.slice(-4);\n}\n","import type { Command } from \"commander\";\nimport { createInterface } from \"node:readline\";\nimport { ApiClient } from \"../api/client.js\";\nimport { loadConfig, saveConfig, resolveApiUrl } from \"../config.js\";\nimport { handleCommandError } from \"../utils/errors.js\";\nimport * as out from \"../utils/output.js\";\n\nexport function registerLoginCommand(program: Command): void {\n const cmd = program\n .command(\"login\")\n .description(\"Authenticate with your API key\")\n .action(async () => {\n const globals = cmd.optsWithGlobals() as Record<string, string | undefined>;\n const apiUrl = resolveApiUrl(globals[\"apiUrl\"]);\n\n // Derive the web app URL from the API URL\n const appUrl = apiUrl.replace(/\\/api\\/v1\\/?$/, \"/app/api-keys\");\n\n if (!process.stdin.isTTY) {\n out.error(\n \"Non-interactive terminal detected. Use --key flag instead:\\n\" +\n \" shorten --key sk_your_key whoami\",\n );\n process.exit(1);\n return;\n }\n\n console.log(`\\nOpen your browser to get an API key:\\n ${appUrl}\\n`);\n\n // Try to open browser automatically (best-effort)\n try {\n const { exec } = await import(\"node:child_process\");\n const openCmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${openCmd} ${appUrl}`);\n } catch {\n // Ignore β€” user can open manually\n }\n\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const key = await new Promise<string>((resolve) => {\n rl.question(\"Paste your API key: \", (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n\n if (!key) {\n out.error(\"No key provided.\");\n process.exit(1);\n return;\n }\n\n if (!key.startsWith(\"sk_\")) {\n out.error('Invalid API key format. Keys start with \"sk_\".');\n process.exit(1);\n return;\n }\n\n // Validate the key by calling /usage, then save on success\n try {\n const client = new ApiClient(key, apiUrl);\n await client.get(\"/usage\");\n\n const config = loadConfig();\n config.api_key = key;\n saveConfig(config);\n\n out.success(\"Logged in successfully. API key saved to ~/.shorten.json\");\n } catch (err) {\n out.error(\"Could not verify the API key.\");\n handleCommandError(err);\n }\n });\n}\n","import type { Command } from \"commander\";\nimport { unlinkSync } from \"node:fs\";\nimport {\n loadConfig,\n saveConfig,\n CONFIG_PATH,\n type ShortenConfig,\n} from \"../config.js\";\nimport * as out from \"../utils/output.js\";\n\nconst VALID_KEYS: (keyof ShortenConfig)[] = [\n \"api_key\",\n \"api_url\",\n \"default_format\",\n \"copy_to_clipboard\",\n];\n\nfunction maskValue(key: string, value: unknown): string {\n if (key === \"api_key\" && typeof value === \"string\" && value.length > 8) {\n return value.slice(0, 3) + \"…\" + value.slice(-4);\n }\n return String(value);\n}\n\nexport function registerConfigCommand(program: Command): void {\n const configCmd = program\n .command(\"config\")\n .description(\"Manage CLI configuration\");\n\n configCmd\n .command(\"list\")\n .description(\"Show all configuration values\")\n .action(() => {\n const config = loadConfig();\n const entries = Object.entries(config);\n\n if (entries.length === 0) {\n out.info(`No configuration found. Config file: ${CONFIG_PATH}`);\n return;\n }\n\n for (const [key, value] of entries) {\n console.log(`${key} = ${maskValue(key, value)}`);\n }\n });\n\n configCmd\n .command(\"get <key>\")\n .description(\"Get a configuration value\")\n .action((key: string) => {\n if (!VALID_KEYS.includes(key as keyof ShortenConfig)) {\n out.error(`Unknown config key: \"${key}\". Valid keys: ${VALID_KEYS.join(\", \")}`);\n process.exit(1);\n return;\n }\n\n const config = loadConfig();\n const value = config[key as keyof ShortenConfig];\n\n if (value === undefined) {\n out.info(`\"${key}\" is not set.`);\n return;\n }\n\n console.log(maskValue(key, value));\n });\n\n configCmd\n .command(\"set <key> <value>\")\n .description(\"Set a configuration value\")\n .action((key: string, value: string) => {\n if (!VALID_KEYS.includes(key as keyof ShortenConfig)) {\n out.error(`Unknown config key: \"${key}\". Valid keys: ${VALID_KEYS.join(\", \")}`);\n process.exit(1);\n return;\n }\n\n const config = loadConfig();\n\n if (key === \"copy_to_clipboard\") {\n (config as Record<string, unknown>)[key] = value === \"true\";\n } else {\n (config as Record<string, unknown>)[key] = value;\n }\n\n saveConfig(config);\n out.success(`Set ${key} = ${maskValue(key, value)}`);\n });\n\n configCmd\n .command(\"reset\")\n .description(\"Delete the configuration file\")\n .action(() => {\n try {\n unlinkSync(CONFIG_PATH);\n out.success(`Deleted ${CONFIG_PATH}`);\n } catch {\n out.info(\"No configuration file to delete.\");\n }\n });\n\n configCmd\n .command(\"path\")\n .description(\"Print the configuration file path\")\n .action(() => {\n console.log(CONFIG_PATH);\n });\n}\n","import { createProgram } from \"./cli.js\";\n\nconst program = createProgram();\nprogram.parseAsync(process.argv);\n"],"mappings":"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","names":["program","program","pc","program","pc","program","program","program","program"]}
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@shorten-dev/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shorten URLs from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "shorten": "./dist/index.js"
8
8
  },
9
- "files": ["dist"],
9
+ "files": [
10
+ "dist"
11
+ ],
10
12
  "scripts": {
11
13
  "build": "tsup",
12
14
  "dev": "tsup --watch",
@@ -35,5 +37,9 @@
35
37
  "url": "https://github.com/shorten-dev/shorten.dev",
36
38
  "directory": "packages/cli"
37
39
  },
38
- "keywords": ["url-shortener", "cli", "shorten"]
40
+ "keywords": [
41
+ "url-shortener",
42
+ "cli",
43
+ "shorten"
44
+ ]
39
45
  }