@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 +318 -87
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
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
|
-
|
|
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(`${
|
|
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(`${
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
...init
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 (
|
|
157
|
-
const config =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
392
|
+
"Time window: 7d, 30d, or 90d (default: 7d)"
|
|
276
393
|
).option("-j, --json", "Output as JSON").action(async (slug, opts) => {
|
|
277
|
-
const
|
|
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
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": [
|
|
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": [
|
|
40
|
+
"keywords": [
|
|
41
|
+
"url-shortener",
|
|
42
|
+
"cli",
|
|
43
|
+
"shorten"
|
|
44
|
+
]
|
|
39
45
|
}
|