peakurl 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/bin/peakurl.js +301 -231
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ peakurl create \
|
|
|
34
34
|
|
|
35
35
|
peakurl list
|
|
36
36
|
peakurl whoami
|
|
37
|
+
peakurl logout
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
## Authentication
|
|
@@ -43,6 +44,7 @@ The CLI uses PeakURL bearer API keys and validates them against `GET /api/v1/use
|
|
|
43
44
|
- `--base-url` accepts either the site root, such as `https://peakurl.org`, or the API base URL, such as `https://peakurl.org/api/v1`
|
|
44
45
|
- API keys are opaque 48-character hex tokens
|
|
45
46
|
- Credentials are stored in the standard per-user config location for `peakurl`
|
|
47
|
+
- `peakurl logout` removes the saved config file, but shell environment variables still override auth if they are set
|
|
46
48
|
|
|
47
49
|
For CI or automation, you can also authenticate with environment variables:
|
|
48
50
|
|
|
@@ -57,6 +59,7 @@ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
|
|
|
57
59
|
| ------------------------------ | ---------------------------------------------------------- |
|
|
58
60
|
| `peakurl login` | Validate and save your PeakURL credentials. |
|
|
59
61
|
| `peakurl whoami` | Show the current authenticated account. |
|
|
62
|
+
| `peakurl logout` | Remove saved local CLI credentials. |
|
|
60
63
|
| `peakurl create <url>` | Create a new short link. |
|
|
61
64
|
| `peakurl list` | List links in your account. |
|
|
62
65
|
| `peakurl get <id-or-alias>` | Fetch a single link by ID or alias. |
|
|
@@ -88,6 +91,12 @@ Inspect a link:
|
|
|
88
91
|
peakurl get example
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
Log out from saved local credentials:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
peakurl logout
|
|
98
|
+
```
|
|
99
|
+
|
|
91
100
|
Delete a link:
|
|
92
101
|
|
|
93
102
|
```bash
|
package/bin/peakurl.js
CHANGED
|
@@ -7,13 +7,15 @@ import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
|
7
7
|
// src/lib/errors.ts
|
|
8
8
|
var CliError = class extends Error {
|
|
9
9
|
exitCode;
|
|
10
|
+
kind;
|
|
10
11
|
constructor(message, exitCode = 1, options) {
|
|
11
12
|
super(message, options);
|
|
12
13
|
this.name = "CliError";
|
|
13
14
|
this.exitCode = exitCode;
|
|
15
|
+
this.kind = options?.kind;
|
|
14
16
|
}
|
|
15
17
|
};
|
|
16
|
-
function
|
|
18
|
+
function ensureCliError(error) {
|
|
17
19
|
if (error instanceof CliError) {
|
|
18
20
|
return error;
|
|
19
21
|
}
|
|
@@ -80,18 +82,18 @@ function normalizeDestinationUrl(value) {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// src/api/client.ts
|
|
83
|
-
function
|
|
85
|
+
function isApiResponse(value) {
|
|
84
86
|
return Boolean(
|
|
85
87
|
value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
|
|
86
88
|
);
|
|
87
89
|
}
|
|
88
|
-
function
|
|
90
|
+
function networkError(baseUrl, error) {
|
|
89
91
|
if (error instanceof Error && error.message) {
|
|
90
92
|
return `Could not reach PeakURL at ${baseUrl}. ${error.message}`;
|
|
91
93
|
}
|
|
92
94
|
return `Could not reach PeakURL at ${baseUrl}.`;
|
|
93
95
|
}
|
|
94
|
-
var
|
|
96
|
+
var ApiClient = class {
|
|
95
97
|
/**
|
|
96
98
|
* Creates a client bound to one resolved credential set.
|
|
97
99
|
*
|
|
@@ -131,12 +133,7 @@ var PeakUrlApiClient = class {
|
|
|
131
133
|
* @returns API response envelope containing list data.
|
|
132
134
|
*/
|
|
133
135
|
listUrls(query) {
|
|
134
|
-
return this.request(
|
|
135
|
-
"GET",
|
|
136
|
-
"urls",
|
|
137
|
-
void 0,
|
|
138
|
-
query
|
|
139
|
-
);
|
|
136
|
+
return this.request("GET", "urls", void 0, query);
|
|
140
137
|
}
|
|
141
138
|
/**
|
|
142
139
|
* Loads a single short URL by identifier or alias.
|
|
@@ -191,13 +188,9 @@ var PeakUrlApiClient = class {
|
|
|
191
188
|
body: body ? JSON.stringify(body) : void 0
|
|
192
189
|
});
|
|
193
190
|
} catch (error) {
|
|
194
|
-
throw new CliError(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{
|
|
198
|
-
cause: error instanceof Error ? error : void 0
|
|
199
|
-
}
|
|
200
|
-
);
|
|
191
|
+
throw new CliError(networkError(this.config.baseUrl, error), 1, {
|
|
192
|
+
cause: error instanceof Error ? error : void 0
|
|
193
|
+
});
|
|
201
194
|
}
|
|
202
195
|
const rawText = await response.text();
|
|
203
196
|
if (!rawText) {
|
|
@@ -224,7 +217,7 @@ var PeakUrlApiClient = class {
|
|
|
224
217
|
}
|
|
225
218
|
throw new CliError("PeakURL returned an invalid JSON response.");
|
|
226
219
|
}
|
|
227
|
-
if (!
|
|
220
|
+
if (!isApiResponse(parsed)) {
|
|
228
221
|
throw new CliError(
|
|
229
222
|
"PeakURL returned an unexpected response envelope."
|
|
230
223
|
);
|
|
@@ -241,18 +234,18 @@ var PeakUrlApiClient = class {
|
|
|
241
234
|
};
|
|
242
235
|
|
|
243
236
|
// src/config/store.ts
|
|
244
|
-
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
237
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
245
238
|
import { dirname, join } from "path";
|
|
246
239
|
import envPaths from "env-paths";
|
|
247
240
|
var CONFIG_FILENAME = "config.json";
|
|
248
241
|
var STATE_FILENAME = "state.json";
|
|
249
|
-
function
|
|
242
|
+
function getConfigPath() {
|
|
250
243
|
return join(envPaths("peakurl", { suffix: "" }).config, CONFIG_FILENAME);
|
|
251
244
|
}
|
|
252
|
-
function
|
|
245
|
+
function getStatePath() {
|
|
253
246
|
return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
|
|
254
247
|
}
|
|
255
|
-
async function
|
|
248
|
+
async function ensureParentDir(filePath) {
|
|
256
249
|
const directory = dirname(filePath);
|
|
257
250
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
258
251
|
return directory;
|
|
@@ -264,7 +257,7 @@ var ConfigStore = class {
|
|
|
264
257
|
*
|
|
265
258
|
* @param filePath Optional override used by tests or advanced callers.
|
|
266
259
|
*/
|
|
267
|
-
constructor(filePath =
|
|
260
|
+
constructor(filePath = getConfigPath()) {
|
|
268
261
|
this.filePath = filePath;
|
|
269
262
|
}
|
|
270
263
|
/**
|
|
@@ -312,7 +305,7 @@ var ConfigStore = class {
|
|
|
312
305
|
* @param config Normalized credential set to write.
|
|
313
306
|
*/
|
|
314
307
|
async save(config) {
|
|
315
|
-
const directory = await
|
|
308
|
+
const directory = await ensureParentDir(this.filePath);
|
|
316
309
|
await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
|
|
317
310
|
`, {
|
|
318
311
|
mode: 384
|
|
@@ -323,6 +316,29 @@ var ConfigStore = class {
|
|
|
323
316
|
} catch {
|
|
324
317
|
}
|
|
325
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Removes the stored credential file.
|
|
321
|
+
*
|
|
322
|
+
* @returns `true` when a saved config file existed and was removed.
|
|
323
|
+
* @throws {CliError} When the file exists but cannot be removed.
|
|
324
|
+
*/
|
|
325
|
+
async clear() {
|
|
326
|
+
try {
|
|
327
|
+
await unlink(this.filePath);
|
|
328
|
+
return true;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code === "ENOENT") {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
throw new CliError(
|
|
334
|
+
`Could not remove PeakURL config at ${this.filePath}.`,
|
|
335
|
+
1,
|
|
336
|
+
{
|
|
337
|
+
cause: error instanceof Error ? error : void 0
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
326
342
|
};
|
|
327
343
|
var StateStore = class {
|
|
328
344
|
filePath;
|
|
@@ -331,7 +347,7 @@ var StateStore = class {
|
|
|
331
347
|
*
|
|
332
348
|
* @param filePath Optional override used by tests or advanced callers.
|
|
333
349
|
*/
|
|
334
|
-
constructor(filePath =
|
|
350
|
+
constructor(filePath = getStatePath()) {
|
|
335
351
|
this.filePath = filePath;
|
|
336
352
|
}
|
|
337
353
|
/**
|
|
@@ -357,7 +373,7 @@ var StateStore = class {
|
|
|
357
373
|
* @param state State payload to save.
|
|
358
374
|
*/
|
|
359
375
|
async save(state) {
|
|
360
|
-
const directory = await
|
|
376
|
+
const directory = await ensureParentDir(this.filePath);
|
|
361
377
|
await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
|
|
362
378
|
`, {
|
|
363
379
|
mode: 384
|
|
@@ -371,7 +387,16 @@ var StateStore = class {
|
|
|
371
387
|
};
|
|
372
388
|
|
|
373
389
|
// src/lib/auth.ts
|
|
374
|
-
|
|
390
|
+
var AUTH_REQUIRED_MESSAGE = "PeakURL credentials are not configured.";
|
|
391
|
+
function authRows() {
|
|
392
|
+
return [
|
|
393
|
+
["Reason", AUTH_REQUIRED_MESSAGE],
|
|
394
|
+
["Command", "peakurl login"],
|
|
395
|
+
["Flags", "--base-url <url> --api-key <key>"],
|
|
396
|
+
["Env", "PEAKURL_BASE_URL, PEAKURL_API_KEY"]
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
function getLoginConfig(input, env) {
|
|
375
400
|
const baseUrl = input.baseUrl?.trim() || env.PEAKURL_BASE_URL?.trim();
|
|
376
401
|
const apiKey = input.apiKey?.trim() || env.PEAKURL_API_KEY?.trim();
|
|
377
402
|
if (!baseUrl || !apiKey) {
|
|
@@ -384,14 +409,14 @@ function resolveLoginConfig(input, env) {
|
|
|
384
409
|
apiKey
|
|
385
410
|
};
|
|
386
411
|
}
|
|
387
|
-
async function
|
|
412
|
+
async function getAuthConfig(env, store = new ConfigStore()) {
|
|
388
413
|
const saved = await store.load();
|
|
389
414
|
const baseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.baseUrl;
|
|
390
415
|
const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
|
|
391
416
|
if (!baseUrl || !apiKey) {
|
|
392
|
-
throw new CliError(
|
|
393
|
-
|
|
394
|
-
);
|
|
417
|
+
throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
|
|
418
|
+
kind: "auth_required"
|
|
419
|
+
});
|
|
395
420
|
}
|
|
396
421
|
return {
|
|
397
422
|
baseUrl: normalizeBaseUrl(baseUrl),
|
|
@@ -507,18 +532,18 @@ function writeJson(value) {
|
|
|
507
532
|
|
|
508
533
|
// src/lib/links.ts
|
|
509
534
|
var LIST_KEYS = ["urls", "items", "results"];
|
|
510
|
-
function
|
|
535
|
+
function asObject(value) {
|
|
511
536
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
512
537
|
}
|
|
513
|
-
function
|
|
538
|
+
function asString(value) {
|
|
514
539
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
515
540
|
}
|
|
516
|
-
function
|
|
541
|
+
function asNumber(value) {
|
|
517
542
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
518
543
|
}
|
|
519
|
-
function
|
|
544
|
+
function pickText(link, keys) {
|
|
520
545
|
for (const key of keys) {
|
|
521
|
-
const value =
|
|
546
|
+
const value = asString(link[key]);
|
|
522
547
|
if (value) {
|
|
523
548
|
return value;
|
|
524
549
|
}
|
|
@@ -528,32 +553,32 @@ function pickString(link, keys) {
|
|
|
528
553
|
function truncate(value, maxLength) {
|
|
529
554
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
530
555
|
}
|
|
531
|
-
function
|
|
532
|
-
const record =
|
|
556
|
+
function getListMeta(data) {
|
|
557
|
+
const record = asObject(data);
|
|
533
558
|
if (!record) {
|
|
534
559
|
return null;
|
|
535
560
|
}
|
|
536
|
-
const meta =
|
|
561
|
+
const meta = asObject(record.meta);
|
|
537
562
|
if (meta) {
|
|
538
563
|
return {
|
|
539
|
-
page:
|
|
540
|
-
limit:
|
|
541
|
-
totalItems:
|
|
542
|
-
totalPages:
|
|
564
|
+
page: asNumber(meta.page),
|
|
565
|
+
limit: asNumber(meta.limit),
|
|
566
|
+
totalItems: asNumber(meta.totalItems),
|
|
567
|
+
totalPages: asNumber(meta.totalPages)
|
|
543
568
|
};
|
|
544
569
|
}
|
|
545
570
|
return {
|
|
546
|
-
page:
|
|
547
|
-
limit:
|
|
548
|
-
totalItems:
|
|
549
|
-
totalPages:
|
|
571
|
+
page: asNumber(record.page),
|
|
572
|
+
limit: asNumber(record.limit),
|
|
573
|
+
totalItems: asNumber(record.total),
|
|
574
|
+
totalPages: asNumber(record.totalPages)
|
|
550
575
|
};
|
|
551
576
|
}
|
|
552
577
|
function extractLinks(data) {
|
|
553
578
|
if (Array.isArray(data)) {
|
|
554
579
|
return data;
|
|
555
580
|
}
|
|
556
|
-
const record =
|
|
581
|
+
const record = asObject(data);
|
|
557
582
|
if (record) {
|
|
558
583
|
for (const key of LIST_KEYS) {
|
|
559
584
|
const value = record[key];
|
|
@@ -565,16 +590,16 @@ function extractLinks(data) {
|
|
|
565
590
|
return [];
|
|
566
591
|
}
|
|
567
592
|
function getLinkId(link) {
|
|
568
|
-
return
|
|
593
|
+
return pickText(link, ["id", "_id", "urlId"]) || (link.id !== void 0 ? String(link.id) : void 0);
|
|
569
594
|
}
|
|
570
595
|
function getLinkAlias(link) {
|
|
571
|
-
return
|
|
596
|
+
return pickText(link, ["alias", "shortCode", "slug", "code"]);
|
|
572
597
|
}
|
|
573
598
|
function getLinkShortUrl(link) {
|
|
574
|
-
return
|
|
599
|
+
return pickText(link, ["shortUrl", "shortLink", "shortURL", "url"]);
|
|
575
600
|
}
|
|
576
601
|
function getLinkDestination(link) {
|
|
577
|
-
return
|
|
602
|
+
return pickText(link, [
|
|
578
603
|
"destinationUrl",
|
|
579
604
|
"originalUrl",
|
|
580
605
|
"targetUrl",
|
|
@@ -590,14 +615,14 @@ function formatLinkDetails(link) {
|
|
|
590
615
|
["Alias", getLinkAlias(link)],
|
|
591
616
|
["Short URL", getLinkShortUrl(link)],
|
|
592
617
|
["Destination", getLinkDestination(link)],
|
|
593
|
-
["Title",
|
|
594
|
-
["Status",
|
|
618
|
+
["Title", asString(link.title)],
|
|
619
|
+
["Status", asString(link.status)],
|
|
595
620
|
[
|
|
596
621
|
"Clicks",
|
|
597
|
-
|
|
622
|
+
asNumber(link.clicks) === void 0 ? void 0 : String(link.clicks)
|
|
598
623
|
],
|
|
599
|
-
["Created",
|
|
600
|
-
["Updated",
|
|
624
|
+
["Created", asString(link.createdAt)],
|
|
625
|
+
["Updated", asString(link.updatedAt)]
|
|
601
626
|
].filter((entry) => Boolean(entry[1]));
|
|
602
627
|
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
603
628
|
}
|
|
@@ -611,12 +636,12 @@ function formatLinksTable(links) {
|
|
|
611
636
|
truncate(getLinkAlias(link) || "-", 12),
|
|
612
637
|
truncate(getLinkShortUrl(link) || "-", 36),
|
|
613
638
|
truncate(getLinkDestination(link) || "-", 52),
|
|
614
|
-
truncate(
|
|
639
|
+
truncate(asString(link.status) || "-", 12)
|
|
615
640
|
]);
|
|
616
641
|
return formatTable(headers, rows);
|
|
617
642
|
}
|
|
618
643
|
function formatListSummary(data, count) {
|
|
619
|
-
const meta =
|
|
644
|
+
const meta = getListMeta(data);
|
|
620
645
|
if (!meta) {
|
|
621
646
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
622
647
|
}
|
|
@@ -629,159 +654,6 @@ function formatListSummary(data, count) {
|
|
|
629
654
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
630
655
|
}
|
|
631
656
|
|
|
632
|
-
// src/commands/create.ts
|
|
633
|
-
function normalizeExpiresAt(value) {
|
|
634
|
-
if (!value) {
|
|
635
|
-
return void 0;
|
|
636
|
-
}
|
|
637
|
-
if (Number.isNaN(Date.parse(value))) {
|
|
638
|
-
throw new CliError(`Invalid expiration timestamp: ${value}`);
|
|
639
|
-
}
|
|
640
|
-
return value;
|
|
641
|
-
}
|
|
642
|
-
async function createCommand(destinationUrl, options) {
|
|
643
|
-
const config = await resolveStoredConfig(process.env);
|
|
644
|
-
const response = await new PeakUrlApiClient(config).createUrl({
|
|
645
|
-
destinationUrl: normalizeDestinationUrl(destinationUrl),
|
|
646
|
-
...options.alias ? { alias: options.alias } : {},
|
|
647
|
-
...options.title ? { title: options.title } : {},
|
|
648
|
-
...options.password ? { password: options.password } : {},
|
|
649
|
-
...options.status ? { status: options.status } : {},
|
|
650
|
-
...options.expiresAt ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
|
|
651
|
-
...options.utmSource ? { utmSource: options.utmSource } : {},
|
|
652
|
-
...options.utmMedium ? { utmMedium: options.utmMedium } : {},
|
|
653
|
-
...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
|
|
654
|
-
...options.utmTerm ? { utmTerm: options.utmTerm } : {},
|
|
655
|
-
...options.utmContent ? { utmContent: options.utmContent } : {}
|
|
656
|
-
});
|
|
657
|
-
if (options.json) {
|
|
658
|
-
writeJson(response);
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (options.quiet) {
|
|
662
|
-
writeStdout(getQuietLinkValue(response.data));
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
writeStdout(response.message);
|
|
666
|
-
writeStdout(formatLinkDetails(response.data));
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// src/commands/delete.ts
|
|
670
|
-
async function deleteCommand(idOrAlias, options) {
|
|
671
|
-
const config = await resolveStoredConfig(process.env);
|
|
672
|
-
const client = new PeakUrlApiClient(config);
|
|
673
|
-
const lookupResponse = await client.getUrl(idOrAlias);
|
|
674
|
-
const resolvedId = getLinkId(lookupResponse.data);
|
|
675
|
-
if (!resolvedId) {
|
|
676
|
-
throw new CliError(
|
|
677
|
-
"PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
const response = await client.deleteUrl(resolvedId);
|
|
681
|
-
if (options.json) {
|
|
682
|
-
writeJson(response);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
if (options.quiet) {
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
writeStdout(response.message);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// src/commands/get.ts
|
|
692
|
-
async function getCommand(idOrAlias, options) {
|
|
693
|
-
const config = await resolveStoredConfig(process.env);
|
|
694
|
-
const response = await new PeakUrlApiClient(config).getUrl(idOrAlias);
|
|
695
|
-
if (options.json) {
|
|
696
|
-
writeJson(response);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (options.quiet) {
|
|
700
|
-
writeStdout(getQuietLinkValue(response.data));
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
writeStdout(response.message);
|
|
704
|
-
writeStdout(formatLinkDetails(response.data));
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// src/commands/list.ts
|
|
708
|
-
async function listCommand(options) {
|
|
709
|
-
const config = await resolveStoredConfig(process.env);
|
|
710
|
-
const response = await new PeakUrlApiClient(config).listUrls({
|
|
711
|
-
page: options.page,
|
|
712
|
-
limit: options.limit,
|
|
713
|
-
search: options.search,
|
|
714
|
-
sortBy: options.sortBy,
|
|
715
|
-
sortOrder: options.sortOrder
|
|
716
|
-
});
|
|
717
|
-
const links = extractLinks(response.data);
|
|
718
|
-
if (options.json) {
|
|
719
|
-
writeJson(response);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
if (options.quiet) {
|
|
723
|
-
for (const link of links) {
|
|
724
|
-
const value = getQuietLinkValue(link);
|
|
725
|
-
if (value) {
|
|
726
|
-
writeStdout(value);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
writeStdout(response.message);
|
|
732
|
-
writeStdout(formatLinksTable(links));
|
|
733
|
-
writeStdout(formatListSummary(response.data, links.length));
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// src/lib/users.ts
|
|
737
|
-
function stringValue2(value) {
|
|
738
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
739
|
-
}
|
|
740
|
-
function getUserLabel(user) {
|
|
741
|
-
const fullName = [stringValue2(user.firstName), stringValue2(user.lastName)].filter(Boolean).join(" ");
|
|
742
|
-
return fullName || stringValue2(user.username) || stringValue2(user.email) || String(user.id ?? "unknown");
|
|
743
|
-
}
|
|
744
|
-
function getQuietUserValue(user) {
|
|
745
|
-
return stringValue2(user.username) || stringValue2(user.email) || String(user.id ?? "");
|
|
746
|
-
}
|
|
747
|
-
function formatUserDetails(user) {
|
|
748
|
-
const lines = [
|
|
749
|
-
["Name", getUserLabel(user)],
|
|
750
|
-
["Username", stringValue2(user.username)],
|
|
751
|
-
["Email", stringValue2(user.email)],
|
|
752
|
-
["Role", stringValue2(user.role)],
|
|
753
|
-
["ID", user.id === void 0 ? void 0 : String(user.id)]
|
|
754
|
-
].filter((entry) => Boolean(entry[1]));
|
|
755
|
-
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// src/commands/login.ts
|
|
759
|
-
async function loginCommand(options) {
|
|
760
|
-
const credentials = resolveLoginConfig(options, process.env);
|
|
761
|
-
const client = new PeakUrlApiClient(credentials);
|
|
762
|
-
const response = await client.whoami();
|
|
763
|
-
await new ConfigStore().save(credentials);
|
|
764
|
-
const responseBody = {
|
|
765
|
-
success: true,
|
|
766
|
-
message: `Saved credentials for ${credentials.baseUrl}.`,
|
|
767
|
-
data: {
|
|
768
|
-
baseUrl: credentials.baseUrl,
|
|
769
|
-
user: response.data
|
|
770
|
-
},
|
|
771
|
-
timestamp: response.timestamp
|
|
772
|
-
};
|
|
773
|
-
if (options.json) {
|
|
774
|
-
writeJson(responseBody);
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
if (options.quiet) {
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
writeStdout(`Saved credentials for ${credentials.baseUrl}`);
|
|
781
|
-
writeStdout(`Authenticated as ${getUserLabel(response.data)}`);
|
|
782
|
-
writeStdout(formatUserDetails(response.data));
|
|
783
|
-
}
|
|
784
|
-
|
|
785
657
|
// src/lib/update.ts
|
|
786
658
|
var PACKAGE_NAME = "peakurl";
|
|
787
659
|
var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -886,7 +758,7 @@ function compareSemver(left, right) {
|
|
|
886
758
|
function getUpdateInstallCommand() {
|
|
887
759
|
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
888
760
|
}
|
|
889
|
-
async function
|
|
761
|
+
async function fetchLatestVersion(env) {
|
|
890
762
|
const registryUrl = normalizeRegistryUrl(getRegistryBaseUrl(env));
|
|
891
763
|
const url = `${registryUrl}/${PACKAGE_NAME}/latest`;
|
|
892
764
|
const controller = new AbortController();
|
|
@@ -909,7 +781,7 @@ async function fetchLatestPackageVersion(env) {
|
|
|
909
781
|
clearTimeout(timeout);
|
|
910
782
|
}
|
|
911
783
|
}
|
|
912
|
-
async function
|
|
784
|
+
async function getUpdateState(store) {
|
|
913
785
|
const state = await store.load();
|
|
914
786
|
return state.update ?? {};
|
|
915
787
|
}
|
|
@@ -920,15 +792,15 @@ async function saveUpdateState(store, update) {
|
|
|
920
792
|
update
|
|
921
793
|
});
|
|
922
794
|
}
|
|
923
|
-
async function
|
|
795
|
+
async function loadLatestVersion(env, options) {
|
|
924
796
|
const store = options?.store ?? new StateStore();
|
|
925
|
-
const updateState = await
|
|
797
|
+
const updateState = await getUpdateState(store);
|
|
926
798
|
const lastCheckedAt = parseTime(updateState.lastCheckedAt);
|
|
927
799
|
const now = Date.now();
|
|
928
800
|
if (!options?.forceRefresh && updateState.latestVersion && lastCheckedAt !== null && now - lastCheckedAt < CACHE_TTL_MS) {
|
|
929
801
|
return updateState.latestVersion;
|
|
930
802
|
}
|
|
931
|
-
const latestVersion = await
|
|
803
|
+
const latestVersion = await fetchLatestVersion(env);
|
|
932
804
|
if (!latestVersion) {
|
|
933
805
|
return updateState.latestVersion ?? null;
|
|
934
806
|
}
|
|
@@ -940,7 +812,7 @@ async function getLatestPackageVersion(env, options) {
|
|
|
940
812
|
return latestVersion;
|
|
941
813
|
}
|
|
942
814
|
async function getUpdateStatus(currentVersion, env, options) {
|
|
943
|
-
const resolvedLatestVersion = await
|
|
815
|
+
const resolvedLatestVersion = await loadLatestVersion(env, {
|
|
944
816
|
forceRefresh: options?.forceRefresh,
|
|
945
817
|
store: options?.store
|
|
946
818
|
});
|
|
@@ -957,13 +829,13 @@ async function getUpdateStatus(currentVersion, env, options) {
|
|
|
957
829
|
installCommand: getUpdateInstallCommand()
|
|
958
830
|
};
|
|
959
831
|
}
|
|
960
|
-
function
|
|
832
|
+
function showUpdateNotice(status) {
|
|
961
833
|
writeNoticeBox("Update Available", [
|
|
962
834
|
`${PACKAGE_NAME} ${status.currentVersion} -> ${status.latestVersion}`,
|
|
963
835
|
`Run: ${status.installCommand}`
|
|
964
836
|
]);
|
|
965
837
|
}
|
|
966
|
-
async function
|
|
838
|
+
async function checkUpdates(options) {
|
|
967
839
|
if (options.env.PEAKURL_DISABLE_UPDATE_CHECK === "1" || options.commandName === "update" || options.options?.json || options.options?.quiet) {
|
|
968
840
|
return;
|
|
969
841
|
}
|
|
@@ -971,7 +843,7 @@ async function maybeShowUpdateNotice(options) {
|
|
|
971
843
|
return;
|
|
972
844
|
}
|
|
973
845
|
const store = new StateStore();
|
|
974
|
-
const updateState = await
|
|
846
|
+
const updateState = await getUpdateState(store);
|
|
975
847
|
const status = await getUpdateStatus(options.currentVersion, options.env, {
|
|
976
848
|
store
|
|
977
849
|
});
|
|
@@ -988,7 +860,198 @@ async function maybeShowUpdateNotice(options) {
|
|
|
988
860
|
lastNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
989
861
|
lastNotifiedVersion: status.latestVersion
|
|
990
862
|
});
|
|
991
|
-
|
|
863
|
+
showUpdateNotice(status);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/lib/users.ts
|
|
867
|
+
function text(value) {
|
|
868
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
869
|
+
}
|
|
870
|
+
function userLabel(user) {
|
|
871
|
+
const fullName = [text(user.firstName), text(user.lastName)].filter((value) => Boolean(value)).join(" ");
|
|
872
|
+
return fullName || text(user.username) || text(user.email) || String(user.id ?? "unknown");
|
|
873
|
+
}
|
|
874
|
+
function userValue(user) {
|
|
875
|
+
return text(user.username) || text(user.email) || String(user.id ?? "");
|
|
876
|
+
}
|
|
877
|
+
function userTable(user) {
|
|
878
|
+
const rows = [
|
|
879
|
+
["Name", userLabel(user)],
|
|
880
|
+
["Username", text(user.username)],
|
|
881
|
+
["Email", text(user.email)],
|
|
882
|
+
["Role", text(user.role)],
|
|
883
|
+
["ID", user.id === void 0 ? void 0 : String(user.id)]
|
|
884
|
+
].filter((entry) => Boolean(entry[1]));
|
|
885
|
+
if (rows.length === 0) {
|
|
886
|
+
return "No user fields returned.";
|
|
887
|
+
}
|
|
888
|
+
return formatTable(["Field", "Value"], rows);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/commands/create.ts
|
|
892
|
+
function normalizeExpiresAt(value) {
|
|
893
|
+
if (!value) {
|
|
894
|
+
return void 0;
|
|
895
|
+
}
|
|
896
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
897
|
+
throw new CliError(`Invalid expiration timestamp: ${value}`);
|
|
898
|
+
}
|
|
899
|
+
return value;
|
|
900
|
+
}
|
|
901
|
+
async function createCommand(destinationUrl, options) {
|
|
902
|
+
const config = await getAuthConfig(process.env);
|
|
903
|
+
const response = await new ApiClient(config).createUrl({
|
|
904
|
+
destinationUrl: normalizeDestinationUrl(destinationUrl),
|
|
905
|
+
...options.alias ? { alias: options.alias } : {},
|
|
906
|
+
...options.title ? { title: options.title } : {},
|
|
907
|
+
...options.password ? { password: options.password } : {},
|
|
908
|
+
...options.status ? { status: options.status } : {},
|
|
909
|
+
...options.expiresAt ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
|
|
910
|
+
...options.utmSource ? { utmSource: options.utmSource } : {},
|
|
911
|
+
...options.utmMedium ? { utmMedium: options.utmMedium } : {},
|
|
912
|
+
...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
|
|
913
|
+
...options.utmTerm ? { utmTerm: options.utmTerm } : {},
|
|
914
|
+
...options.utmContent ? { utmContent: options.utmContent } : {}
|
|
915
|
+
});
|
|
916
|
+
if (options.json) {
|
|
917
|
+
writeJson(response);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (options.quiet) {
|
|
921
|
+
writeStdout(getQuietLinkValue(response.data));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
writeStdout(response.message);
|
|
925
|
+
writeStdout(formatLinkDetails(response.data));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/commands/delete.ts
|
|
929
|
+
async function deleteCommand(idOrAlias, options) {
|
|
930
|
+
const config = await getAuthConfig(process.env);
|
|
931
|
+
const client = new ApiClient(config);
|
|
932
|
+
const lookupResponse = await client.getUrl(idOrAlias);
|
|
933
|
+
const resolvedId = getLinkId(lookupResponse.data);
|
|
934
|
+
if (!resolvedId) {
|
|
935
|
+
throw new CliError(
|
|
936
|
+
"PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const response = await client.deleteUrl(resolvedId);
|
|
940
|
+
if (options.json) {
|
|
941
|
+
writeJson(response);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (options.quiet) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
writeStdout(response.message);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/commands/get.ts
|
|
951
|
+
async function getCommand(idOrAlias, options) {
|
|
952
|
+
const config = await getAuthConfig(process.env);
|
|
953
|
+
const response = await new ApiClient(config).getUrl(idOrAlias);
|
|
954
|
+
if (options.json) {
|
|
955
|
+
writeJson(response);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (options.quiet) {
|
|
959
|
+
writeStdout(getQuietLinkValue(response.data));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
writeStdout(response.message);
|
|
963
|
+
writeStdout(formatLinkDetails(response.data));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/commands/list.ts
|
|
967
|
+
async function listCommand(options) {
|
|
968
|
+
const config = await getAuthConfig(process.env);
|
|
969
|
+
const response = await new ApiClient(config).listUrls({
|
|
970
|
+
page: options.page,
|
|
971
|
+
limit: options.limit,
|
|
972
|
+
search: options.search,
|
|
973
|
+
sortBy: options.sortBy,
|
|
974
|
+
sortOrder: options.sortOrder
|
|
975
|
+
});
|
|
976
|
+
const links = extractLinks(response.data);
|
|
977
|
+
if (options.json) {
|
|
978
|
+
writeJson(response);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (options.quiet) {
|
|
982
|
+
for (const link of links) {
|
|
983
|
+
const value = getQuietLinkValue(link);
|
|
984
|
+
if (value) {
|
|
985
|
+
writeStdout(value);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
writeStdout(response.message);
|
|
991
|
+
writeStdout(formatLinksTable(links));
|
|
992
|
+
writeStdout(formatListSummary(response.data, links.length));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/commands/login.ts
|
|
996
|
+
async function loginCommand(options) {
|
|
997
|
+
const credentials = getLoginConfig(options, process.env);
|
|
998
|
+
const client = new ApiClient(credentials);
|
|
999
|
+
const response = await client.whoami();
|
|
1000
|
+
await new ConfigStore().save(credentials);
|
|
1001
|
+
const responseBody = {
|
|
1002
|
+
success: true,
|
|
1003
|
+
message: `Saved credentials for ${credentials.baseUrl}.`,
|
|
1004
|
+
data: {
|
|
1005
|
+
baseUrl: credentials.baseUrl,
|
|
1006
|
+
user: response.data
|
|
1007
|
+
},
|
|
1008
|
+
timestamp: response.timestamp
|
|
1009
|
+
};
|
|
1010
|
+
if (options.json) {
|
|
1011
|
+
writeJson(responseBody);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (options.quiet) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
writeStdout(`Saved credentials for ${credentials.baseUrl}`);
|
|
1018
|
+
writeStdout(`Authenticated as ${userLabel(response.data)}`);
|
|
1019
|
+
writeStdout(userTable(response.data));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/commands/logout.ts
|
|
1023
|
+
function hasEnvConfig(env) {
|
|
1024
|
+
return Boolean(env.PEAKURL_BASE_URL?.trim() || env.PEAKURL_API_KEY?.trim());
|
|
1025
|
+
}
|
|
1026
|
+
async function logoutCommand(options) {
|
|
1027
|
+
const store = new ConfigStore();
|
|
1028
|
+
const saved = await store.load();
|
|
1029
|
+
const removed = await store.clear();
|
|
1030
|
+
const envConfig = hasEnvConfig(process.env);
|
|
1031
|
+
const message = removed && saved?.baseUrl ? `Logged out. Removed saved credentials for ${saved.baseUrl}.` : removed ? "Logged out. Removed saved PeakURL credentials." : "Already logged out. No saved PeakURL credentials were found.";
|
|
1032
|
+
const responseBody = {
|
|
1033
|
+
success: true,
|
|
1034
|
+
message,
|
|
1035
|
+
data: {
|
|
1036
|
+
removed,
|
|
1037
|
+
baseUrl: saved?.baseUrl,
|
|
1038
|
+
envCredentialsActive: envConfig
|
|
1039
|
+
},
|
|
1040
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1041
|
+
};
|
|
1042
|
+
if (options.json) {
|
|
1043
|
+
writeJson(responseBody);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (options.quiet) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
writeStdout(message);
|
|
1050
|
+
if (envConfig) {
|
|
1051
|
+
writeStdout(
|
|
1052
|
+
"Environment credentials in PEAKURL_BASE_URL or PEAKURL_API_KEY still apply in this shell."
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
992
1055
|
}
|
|
993
1056
|
|
|
994
1057
|
// src/commands/update.ts
|
|
@@ -1034,22 +1097,22 @@ async function updateCommand(options, currentVersion) {
|
|
|
1034
1097
|
|
|
1035
1098
|
// src/commands/whoami.ts
|
|
1036
1099
|
async function whoamiCommand(options) {
|
|
1037
|
-
const config = await
|
|
1038
|
-
const response = await new
|
|
1100
|
+
const config = await getAuthConfig(process.env);
|
|
1101
|
+
const response = await new ApiClient(config).whoami();
|
|
1039
1102
|
if (options.json) {
|
|
1040
1103
|
writeJson(response);
|
|
1041
1104
|
return;
|
|
1042
1105
|
}
|
|
1043
1106
|
if (options.quiet) {
|
|
1044
|
-
writeStdout(
|
|
1107
|
+
writeStdout(userValue(response.data));
|
|
1045
1108
|
return;
|
|
1046
1109
|
}
|
|
1047
1110
|
writeStdout(response.message);
|
|
1048
|
-
writeStdout(
|
|
1111
|
+
writeStdout(userTable(response.data));
|
|
1049
1112
|
}
|
|
1050
1113
|
|
|
1051
1114
|
// src/index.ts
|
|
1052
|
-
function
|
|
1115
|
+
function parseNumber(label) {
|
|
1053
1116
|
return (value) => {
|
|
1054
1117
|
const parsed = Number.parseInt(value, 10);
|
|
1055
1118
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -1075,6 +1138,7 @@ async function main() {
|
|
|
1075
1138
|
Examples:
|
|
1076
1139
|
peakurl login --base-url https://peakurl.org --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
1077
1140
|
peakurl whoami --json
|
|
1141
|
+
peakurl logout
|
|
1078
1142
|
peakurl create https://example.com --alias example
|
|
1079
1143
|
peakurl list --limit 10
|
|
1080
1144
|
peakurl update --check
|
|
@@ -1083,7 +1147,7 @@ Examples:
|
|
|
1083
1147
|
).exitOverride();
|
|
1084
1148
|
program.hook("preAction", async (_command, actionCommand) => {
|
|
1085
1149
|
const options = actionCommand.optsWithGlobals();
|
|
1086
|
-
await
|
|
1150
|
+
await checkUpdates({
|
|
1087
1151
|
currentVersion: version,
|
|
1088
1152
|
commandName: actionCommand.name(),
|
|
1089
1153
|
options,
|
|
@@ -1097,11 +1161,12 @@ Examples:
|
|
|
1097
1161
|
"PeakURL base URL, for example https://peakurl.org"
|
|
1098
1162
|
).option("--api-key <token>", "PeakURL API key to store").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(loginCommand);
|
|
1099
1163
|
program.command("whoami").description("Show the current authenticated PeakURL user.").option("--json", "Print machine-readable output").option("--quiet", "Print a minimal identity value").action(whoamiCommand);
|
|
1164
|
+
program.command("logout").description("Remove saved PeakURL credentials from this device.").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(logoutCommand);
|
|
1100
1165
|
program.command("create").description("Create a PeakURL short link.").argument("<url>", "Destination URL to shorten").option("--alias <alias>", "Custom alias for the short link").option("--title <title>", "Title to store with the short link").option("--password <password>", "Password-protect the short link").option(
|
|
1101
1166
|
"--status <status>",
|
|
1102
1167
|
"Link status, for example active or paused"
|
|
1103
1168
|
).option("--expires-at <iso>", "Expiration timestamp in ISO-8601 format").option("--utm-source <value>", "UTM source").option("--utm-medium <value>", "UTM medium").option("--utm-campaign <value>", "UTM campaign").option("--utm-term <value>", "UTM term").option("--utm-content <value>", "UTM content").option("--json", "Print machine-readable output").option("--quiet", "Print only the created short URL").action(createCommand);
|
|
1104
|
-
program.command("list").description("List PeakURL short links.").option("--page <number>", "Page number",
|
|
1169
|
+
program.command("list").description("List PeakURL short links.").option("--page <number>", "Page number", parseNumber("page")).option("--limit <number>", "Page size", parseNumber("limit")).option("--search <query>", "Search term").option("--sort-by <field>", "Sort field").option("--sort-order <order>", "Sort order, for example asc or desc").option("--json", "Print machine-readable output").option("--quiet", "Print a minimal per-link value").action(listCommand);
|
|
1105
1170
|
program.command("get").description("Fetch a single PeakURL short link by id or alias.").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Print only the short URL").action(getCommand);
|
|
1106
1171
|
program.command("delete").description("Delete a PeakURL short link by id or alias.").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteCommand);
|
|
1107
1172
|
program.command("update").description(
|
|
@@ -1116,8 +1181,13 @@ Examples:
|
|
|
1116
1181
|
if (error instanceof CommanderError) {
|
|
1117
1182
|
process.exit(error.exitCode);
|
|
1118
1183
|
}
|
|
1119
|
-
const cliError =
|
|
1120
|
-
|
|
1184
|
+
const cliError = ensureCliError(error);
|
|
1185
|
+
if (cliError.kind === "auth_required") {
|
|
1186
|
+
writeStderr("Not logged in.");
|
|
1187
|
+
writeStderr(formatTable(["Field", "Value"], authRows(), "stderr"));
|
|
1188
|
+
} else {
|
|
1189
|
+
writeStderr(cliError.message);
|
|
1190
|
+
}
|
|
1121
1191
|
process.exit(cliError.exitCode);
|
|
1122
1192
|
}
|
|
1123
1193
|
}
|