@shorten-dev/cli 0.1.0 → 0.1.2

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
- // src/api/client.ts
11
- var BASE_URL = "https://shorten.dev/api/v1";
71
+ // ../shared/src/errors.ts
12
72
  var ShortenApiError = class extends Error {
13
73
  constructor(status, body) {
14
74
  super(body.message);
@@ -17,13 +77,25 @@ 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
+ };
88
+
89
+ // src/api/client.ts
20
90
  var ApiClient = class {
21
91
  apiKey;
22
- constructor(apiKey) {
92
+ baseUrl;
93
+ constructor(apiKey, baseUrl) {
23
94
  this.apiKey = apiKey;
95
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
24
96
  }
25
97
  async get(path, params) {
26
- const url = new URL(`${BASE_URL}${path}`);
98
+ const url = new URL(`${this.baseUrl}${path}`);
27
99
  if (params) {
28
100
  for (const [k, v] of Object.entries(params)) {
29
101
  if (v !== void 0 && v !== "") url.searchParams.set(k, v);
@@ -32,39 +104,39 @@ var ApiClient = class {
32
104
  return this.request(url, { method: "GET" });
33
105
  }
34
106
  async post(path, body) {
35
- return this.request(new URL(`${BASE_URL}${path}`), {
107
+ return this.request(new URL(`${this.baseUrl}${path}`), {
36
108
  method: "POST",
37
109
  headers: { "Content-Type": "application/json" },
38
110
  body: body ? JSON.stringify(body) : void 0
39
111
  });
40
112
  }
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
113
  async request(url, init) {
59
- const res = await fetch(url, {
60
- ...init,
61
- headers: {
62
- ...init.headers ?? {},
63
- Authorization: `Bearer ${this.apiKey}`
64
- }
65
- });
114
+ let res;
115
+ try {
116
+ res = await fetch(url, {
117
+ ...init,
118
+ headers: {
119
+ ...init.headers ?? {},
120
+ Authorization: `Bearer ${this.apiKey}`
121
+ }
122
+ });
123
+ } catch (err) {
124
+ throw new NetworkError(
125
+ err instanceof Error ? err : new Error(String(err)),
126
+ url.toString()
127
+ );
128
+ }
66
129
  if (!res.ok) {
67
- const body = await res.json();
130
+ let body;
131
+ try {
132
+ body = await res.json();
133
+ } catch {
134
+ body = {
135
+ error: `HTTP ${res.status}`,
136
+ message: res.statusText || `Request failed with status ${res.status}`,
137
+ status: res.status
138
+ };
139
+ }
68
140
  throw new ShortenApiError(res.status, body);
69
141
  }
70
142
  return await res.json();
@@ -72,9 +144,10 @@ var ApiClient = class {
72
144
  };
73
145
 
74
146
  // src/config.ts
75
- import { readFileSync } from "fs";
147
+ import { readFileSync, writeFileSync } from "fs";
76
148
  import { join } from "path";
77
149
  import { homedir } from "os";
150
+ var DEFAULT_API_URL = "https://shorten.dev/api/v1";
78
151
  var CONFIG_PATH = join(homedir(), ".shorten.json");
79
152
  function loadConfig() {
80
153
  try {
@@ -84,26 +157,31 @@ function loadConfig() {
84
157
  return {};
85
158
  }
86
159
  }
160
+ function saveConfig(config) {
161
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
162
+ }
87
163
  function resolveApiKey(flagKey) {
88
164
  const key = flagKey ?? process.env["SHORTEN_API_KEY"] ?? loadConfig().api_key;
89
165
  if (!key) {
90
166
  console.error(
91
- "No API key found. Set SHORTEN_API_KEY or pass --key.\nGenerate one at https://shorten.dev/api-keys"
167
+ 'No API key found. Set SHORTEN_API_KEY or pass --key.\nRun "shorten login" or visit https://shorten.dev/api-keys'
92
168
  );
93
169
  process.exit(1);
94
170
  }
95
171
  return key;
96
172
  }
173
+ function resolveApiUrl(flagUrl) {
174
+ return flagUrl ?? process.env["SHORTEN_API_URL"] ?? loadConfig().api_url ?? DEFAULT_API_URL;
175
+ }
97
176
 
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
- }
177
+ // src/utils/client-factory.ts
178
+ function resolveClient(cmd) {
179
+ const globals = cmd.optsWithGlobals();
180
+ const config = loadConfig();
181
+ const apiKey = resolveApiKey(globals["key"]);
182
+ const apiUrl = resolveApiUrl(globals["apiUrl"]);
183
+ const client = new ApiClient(apiKey, apiUrl);
184
+ return { client, config, apiKey };
107
185
  }
108
186
 
109
187
  // src/utils/output.ts
@@ -151,18 +229,75 @@ function formatDate(iso) {
151
229
  });
152
230
  }
153
231
 
232
+ // src/utils/errors.ts
233
+ function handleCommandError(err) {
234
+ if (err instanceof ShortenApiError) {
235
+ switch (err.status) {
236
+ case 401:
237
+ error("Invalid API key. Check your key or generate a new one.");
238
+ info(' Run "shorten login" or visit https://shorten.dev/api-keys');
239
+ break;
240
+ case 403:
241
+ error("Forbidden \u2014 your API key doesn't have the required scope.");
242
+ break;
243
+ case 404:
244
+ error(err.message || "Not found.");
245
+ break;
246
+ case 429:
247
+ error("Rate limit exceeded. Try again later.");
248
+ break;
249
+ default:
250
+ error(err.message);
251
+ }
252
+ process.exit(1);
253
+ }
254
+ if (err instanceof NetworkError) {
255
+ error(`Could not connect to the API (${err.url}).`);
256
+ info(" Check your internet connection or use --api-url to set the correct endpoint.");
257
+ process.exit(1);
258
+ }
259
+ const message = err instanceof Error ? err.message : String(err);
260
+ error(`Unexpected error: ${message}`);
261
+ process.exit(1);
262
+ }
263
+
264
+ // src/utils/clipboard.ts
265
+ async function copyToClipboard(text) {
266
+ try {
267
+ const { default: clipboardy } = await import("clipboardy");
268
+ await clipboardy.write(text);
269
+ return true;
270
+ } catch {
271
+ return false;
272
+ }
273
+ }
274
+
154
275
  // src/commands/shorten.ts
276
+ function normalizeUrl(raw) {
277
+ if (!/^https?:\/\//i.test(raw)) {
278
+ return `https://${raw}`;
279
+ }
280
+ return raw;
281
+ }
155
282
  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);
283
+ 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) => {
284
+ if (!rawUrl) {
285
+ program2.help();
286
+ return;
287
+ }
288
+ const { client, config } = resolveClient(cmd);
161
289
  const format = resolveFormat({
162
290
  json: opts.json,
163
291
  quiet: opts.quiet,
164
292
  configDefault: config.default_format
165
293
  });
294
+ const url = normalizeUrl(rawUrl);
295
+ const validationError = validateDestinationUrl(url);
296
+ if (validationError) {
297
+ error(validationError);
298
+ process.exit(1);
299
+ return;
300
+ }
166
301
  const tags = opts.tag;
167
302
  if (tags && tags.length > MAX_TAGS_PER_LINK) {
168
303
  error(`Too many tags: got ${tags.length}, max is ${MAX_TAGS_PER_LINK}`);
@@ -201,12 +336,7 @@ function registerShortenCommand(program2) {
201
336
  info(` tags: ${res.link.tags.join(", ")}`);
202
337
  }
203
338
  } catch (err) {
204
- if (err instanceof ShortenApiError) {
205
- error(err.message);
206
- process.exit(1);
207
- return;
208
- }
209
- throw err;
339
+ handleCommandError(err);
210
340
  }
211
341
  });
212
342
  }
@@ -217,9 +347,7 @@ function registerListCommand(program2) {
217
347
  "--sort <field>",
218
348
  "Sort by created_at or slug"
219
349
  ).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);
350
+ const { client } = resolveClient(cmd);
223
351
  const params = {};
224
352
  if (opts.limit) params["limit"] = opts.limit;
225
353
  if (opts.status) params["status"] = opts.status;
@@ -253,12 +381,7 @@ function registerListCommand(program2) {
253
381
  Showing ${res.data.length} of ${res.total} links (page ${res.page}/${res.total_pages})`
254
382
  );
255
383
  } catch (err) {
256
- if (err instanceof ShortenApiError) {
257
- error(err.message);
258
- process.exit(1);
259
- return;
260
- }
261
- throw err;
384
+ handleCommandError(err);
262
385
  }
263
386
  });
264
387
  }
@@ -272,11 +395,9 @@ import pc2 from "picocolors";
272
395
  function registerStatsCommand(program2) {
273
396
  const cmd = program2.command("stats <slug>").description("View click analytics for a link").option(
274
397
  "-p, --period <period>",
275
- "Time window: 7d, 30d, 90d, or all (default: 7d)"
398
+ "Time window: 7d, 30d, or 90d (default: 7d)"
276
399
  ).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);
400
+ const { client } = resolveClient(cmd);
280
401
  const params = {};
281
402
  if (opts.period) params["period"] = opts.period;
282
403
  try {
@@ -288,10 +409,10 @@ function registerStatsCommand(program2) {
288
409
  json(res);
289
410
  return;
290
411
  }
291
- const period = res.period === "all" ? "all time" : res.period;
412
+ const period = res.period;
292
413
  console.log(
293
414
  `
294
- ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_clicks))} clicks (${period})`
415
+ ${pc2.bold(`r.shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_clicks))} clicks (${period})`
295
416
  );
296
417
  console.log(
297
418
  pc2.dim(
@@ -307,11 +428,10 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
307
428
  console.log(
308
429
  pc2.bold(" Top countries".padEnd(colWidth)) + pc2.bold("Top referrers".padEnd(colWidth)) + pc2.bold("Devices")
309
430
  );
310
- const deviceEntries = [
311
- ["Desktop", res.devices.desktop],
312
- ["Mobile", res.devices.mobile],
313
- ["Tablet", res.devices.tablet]
314
- ];
431
+ const deviceEntries = res.top_devices.slice(0, maxRows).map((d) => [
432
+ d.device.charAt(0).toUpperCase() + d.device.slice(1),
433
+ d.count
434
+ ]);
315
435
  for (let i = 0; i < maxRows; i++) {
316
436
  let line = " ";
317
437
  const country = countries[i];
@@ -319,7 +439,7 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
319
439
  const pct = Math.round(
320
440
  country.count / totalForPct * 100
321
441
  );
322
- line += `${country.country_code.padEnd(6)}${String(pct).padStart(3)}%`.padEnd(
442
+ line += `${country.country.padEnd(6)}${String(pct).padStart(3)}%`.padEnd(
323
443
  colWidth
324
444
  );
325
445
  } else {
@@ -347,12 +467,7 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
347
467
  }
348
468
  console.log();
349
469
  } catch (err) {
350
- if (err instanceof ShortenApiError) {
351
- error(err.message);
352
- process.exit(1);
353
- return;
354
- }
355
- throw err;
470
+ handleCommandError(err);
356
471
  }
357
472
  });
358
473
  }
@@ -361,9 +476,7 @@ ${pc2.bold(`shorten.dev/${res.slug}`)} \u2014 ${pc2.cyan(formatNumber(res.total_
361
476
  import pc3 from "picocolors";
362
477
  function registerWhoamiCommand(program2) {
363
478
  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);
479
+ const { client, apiKey } = resolveClient(cmd);
367
480
  try {
368
481
  const usage = await client.get("/usage");
369
482
  if (opts.json) {
@@ -383,12 +496,7 @@ function registerWhoamiCommand(program2) {
383
496
  ` Resets: ${formatDate(usage.reset_at)}`
384
497
  );
385
498
  } catch (err) {
386
- if (err instanceof ShortenApiError) {
387
- error(err.message);
388
- process.exit(1);
389
- return;
390
- }
391
- throw err;
499
+ handleCommandError(err);
392
500
  }
393
501
  });
394
502
  }
@@ -397,14 +505,143 @@ function maskKey(key) {
397
505
  return key.slice(0, 3) + "\u2026" + key.slice(-4);
398
506
  }
399
507
 
508
+ // src/commands/login.ts
509
+ import { createInterface } from "readline";
510
+ function registerLoginCommand(program2) {
511
+ const cmd = program2.command("login").description("Authenticate with your API key").action(async () => {
512
+ const globals = cmd.optsWithGlobals();
513
+ const apiUrl = resolveApiUrl(globals["apiUrl"]);
514
+ const appUrl = apiUrl.replace(/\/api\/v1\/?$/, "/app/api-keys");
515
+ if (!process.stdin.isTTY) {
516
+ error(
517
+ "Non-interactive terminal detected. Use --key flag instead:\n shorten --key sk_your_key whoami"
518
+ );
519
+ process.exit(1);
520
+ return;
521
+ }
522
+ console.log(`
523
+ Open your browser to get an API key:
524
+ ${appUrl}
525
+ `);
526
+ try {
527
+ const { exec } = await import("child_process");
528
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
529
+ exec(`${openCmd} ${appUrl}`);
530
+ } catch {
531
+ }
532
+ const rl = createInterface({
533
+ input: process.stdin,
534
+ output: process.stdout
535
+ });
536
+ const key = await new Promise((resolve) => {
537
+ rl.question("Paste your API key: ", (answer) => {
538
+ rl.close();
539
+ resolve(answer.trim());
540
+ });
541
+ });
542
+ if (!key) {
543
+ error("No key provided.");
544
+ process.exit(1);
545
+ return;
546
+ }
547
+ if (!key.startsWith("sk_")) {
548
+ error('Invalid API key format. Keys start with "sk_".');
549
+ process.exit(1);
550
+ return;
551
+ }
552
+ try {
553
+ const client = new ApiClient(key, apiUrl);
554
+ await client.get("/usage");
555
+ const config = loadConfig();
556
+ config.api_key = key;
557
+ saveConfig(config);
558
+ success("Logged in successfully. API key saved to ~/.shorten.json");
559
+ } catch (err) {
560
+ error("Could not verify the API key.");
561
+ handleCommandError(err);
562
+ }
563
+ });
564
+ }
565
+
566
+ // src/commands/config.ts
567
+ import { unlinkSync } from "fs";
568
+ var VALID_KEYS = [
569
+ "api_key",
570
+ "api_url",
571
+ "default_format",
572
+ "copy_to_clipboard"
573
+ ];
574
+ function maskValue(key, value) {
575
+ if (key === "api_key" && typeof value === "string" && value.length > 8) {
576
+ return value.slice(0, 3) + "\u2026" + value.slice(-4);
577
+ }
578
+ return String(value);
579
+ }
580
+ function registerConfigCommand(program2) {
581
+ const configCmd = program2.command("config").description("Manage CLI configuration");
582
+ configCmd.command("list").description("Show all configuration values").action(() => {
583
+ const config = loadConfig();
584
+ const entries = Object.entries(config);
585
+ if (entries.length === 0) {
586
+ info(`No configuration found. Config file: ${CONFIG_PATH}`);
587
+ return;
588
+ }
589
+ for (const [key, value] of entries) {
590
+ console.log(`${key} = ${maskValue(key, value)}`);
591
+ }
592
+ });
593
+ configCmd.command("get <key>").description("Get a configuration value").action((key) => {
594
+ if (!VALID_KEYS.includes(key)) {
595
+ error(`Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
596
+ process.exit(1);
597
+ return;
598
+ }
599
+ const config = loadConfig();
600
+ const value = config[key];
601
+ if (value === void 0) {
602
+ info(`"${key}" is not set.`);
603
+ return;
604
+ }
605
+ console.log(maskValue(key, value));
606
+ });
607
+ configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
608
+ if (!VALID_KEYS.includes(key)) {
609
+ error(`Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
610
+ process.exit(1);
611
+ return;
612
+ }
613
+ const config = loadConfig();
614
+ if (key === "copy_to_clipboard") {
615
+ config[key] = value === "true";
616
+ } else {
617
+ config[key] = value;
618
+ }
619
+ saveConfig(config);
620
+ success(`Set ${key} = ${maskValue(key, value)}`);
621
+ });
622
+ configCmd.command("reset").description("Delete the configuration file").action(() => {
623
+ try {
624
+ unlinkSync(CONFIG_PATH);
625
+ success(`Deleted ${CONFIG_PATH}`);
626
+ } catch {
627
+ info("No configuration file to delete.");
628
+ }
629
+ });
630
+ configCmd.command("path").description("Print the configuration file path").action(() => {
631
+ console.log(CONFIG_PATH);
632
+ });
633
+ }
634
+
400
635
  // src/cli.ts
401
636
  function createProgram() {
402
637
  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) });
638
+ program2.name("shorten").description("Shorten URLs from your terminal").version("0.1.2").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
639
  registerShortenCommand(program2);
405
640
  registerListCommand(program2);
406
641
  registerStatsCommand(program2);
407
642
  registerWhoamiCommand(program2);
643
+ registerLoginCommand(program2);
644
+ registerConfigCommand(program2);
408
645
  return program2;
409
646
  }
410
647
 
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","../../shared/src/errors.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://r.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// ── KV cache ──────────────────────────────────────────────────────────\nexport const KV_CACHE_TTL_SECONDS = 86400; // 24 hours\n\n// ── API key format ────────────────────────────────────────────────────\nexport const API_KEY_PREFIX = \"sk_\";\nexport const API_KEY_BYTE_LENGTH = 32;\nexport const API_KEY_PREFIX_DISPLAY_LENGTH = 8;\n\n// ── Click events ──────────────────────────────────────────────────────\nexport const COUNTRY_UNKNOWN = \"XX\";\n\n// ── Pagination ────────────────────────────────────────────────────────\nexport const PAGE_LIMIT_DEFAULT = 10;\nexport const PAGE_LIMIT_MAX = 50;\n\n// ── Postgres / PostgREST error codes ──────────────────────────────────\nexport const PG_UNIQUE_VIOLATION = \"23505\";\nexport const PGRST_NO_ROWS = \"PGRST116\";\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\n// ── Slug path validation ────────────────────────────────────────────────\n\nconst STATIC_EXTENSION = /\\.\\w{1,10}$/;\n\n/**\n * Validate whether a string is a valid short link slug.\n * Accepts both \"/my-slug\" (pathname) and \"my-slug\" (raw slug).\n * Returns the slug if valid, null if reserved/invalid/nested/static.\n *\n * Used by API route handlers for custom slug validation during link creation.\n */\nexport function isSlugPath(input: string): string | null {\n if (input === \"/\") return null;\n\n const segment = input.startsWith(\"/\") ? input.slice(1) : input;\n\n // Nested paths (e.g., /api/v1/links) are never slugs\n if (segment.includes(\"/\")) return null;\n\n // Static assets (e.g., favicon.ico)\n if (STATIC_EXTENSION.test(segment)) return null;\n\n // Must match slug character pattern and length\n if (!SLUG_PATTERN.test(segment)) return null;\n if (segment.length < SLUG_MIN_LENGTH || segment.length > SLUG_MAX_LENGTH) return null;\n\n // Reserved slugs\n if (RESERVED_SLUGS.has(segment.toLowerCase())) return null;\n\n // Brand protection\n if (findBrandTermInSlug(segment)) return null;\n\n return segment;\n}\n","import type { ApiError } from \"./types/api\";\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","import type { ApiError } from \"@shorten/shared\";\nimport { ShortenApiError, NetworkError } from \"@shorten/shared\";\n\nexport { ShortenApiError, NetworkError };\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/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/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 | undefined, opts: ShortenOptions) => {\n if (!rawUrl) {\n program.help();\n return;\n }\n\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(`r.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.2",
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
  }