peakurl 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -29
- package/bin/peakurl.js +486 -70
- package/package.json +17 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PeakURL Command-Line Interface and Abd Ur Rehman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
# PeakURL
|
|
1
|
+
# PeakURL - Command-Line Interface
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official command-line interface for PeakURL.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use `peakurl` to create short links, inspect existing links, and manage your PeakURL account from the terminal.
|
|
6
|
+
|
|
7
|
+
Learn more in the full CLI docs: <https://peakurl.org/docs/cli>
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Node.js 20 or later is required.
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
14
|
npm install -g peakurl
|
|
@@ -14,22 +16,35 @@ npm install -g peakurl
|
|
|
14
16
|
|
|
15
17
|
## Quick Start
|
|
16
18
|
|
|
19
|
+
Sign in with your PeakURL API key:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
peakurl login \
|
|
23
|
+
--base-url https://peakurl.org \
|
|
24
|
+
--api-key 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Create and review a short link:
|
|
28
|
+
|
|
17
29
|
```bash
|
|
18
|
-
peakurl
|
|
19
|
-
|
|
30
|
+
peakurl create \
|
|
31
|
+
https://example.com/articles/launch \
|
|
32
|
+
--alias launch \
|
|
33
|
+
--title "Launch Post"
|
|
34
|
+
|
|
20
35
|
peakurl list
|
|
21
36
|
peakurl whoami
|
|
22
37
|
```
|
|
23
38
|
|
|
24
39
|
## Authentication
|
|
25
40
|
|
|
26
|
-
The CLI uses PeakURL bearer API keys and validates them
|
|
41
|
+
The CLI uses PeakURL bearer API keys and validates them against `GET /api/v1/users/me` before saving credentials.
|
|
27
42
|
|
|
28
|
-
- `--base-url` accepts either the site root such as `https://peakurl.org
|
|
43
|
+
- `--base-url` accepts either the site root, such as `https://peakurl.org`, or the API base URL, such as `https://peakurl.org/api/v1`
|
|
29
44
|
- API keys are opaque 48-character hex tokens
|
|
30
|
-
-
|
|
45
|
+
- Credentials are stored in the standard per-user config location for `peakurl`
|
|
31
46
|
|
|
32
|
-
For CI
|
|
47
|
+
For CI or automation, you can also authenticate with environment variables:
|
|
33
48
|
|
|
34
49
|
```bash
|
|
35
50
|
export PEAKURL_BASE_URL=https://peakurl.org
|
|
@@ -38,32 +53,33 @@ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
|
|
|
38
53
|
|
|
39
54
|
## Commands
|
|
40
55
|
|
|
41
|
-
| Command | Description
|
|
42
|
-
| ------------------------------ |
|
|
43
|
-
| `peakurl login` | Validate and save your PeakURL credentials.
|
|
44
|
-
| `peakurl whoami` | Show the current authenticated account.
|
|
45
|
-
| `peakurl create <url>` | Create a new short link.
|
|
46
|
-
| `peakurl list` | List links in your account.
|
|
47
|
-
| `peakurl get <id-or-alias>` | Fetch a single link by ID or alias.
|
|
48
|
-
| `peakurl delete <id-or-alias>` | Delete a link by ID or alias.
|
|
49
|
-
|
|
50
|
-
## Common Flags
|
|
51
|
-
|
|
52
|
-
- `--json` prints machine-readable JSON output
|
|
53
|
-
- `--quiet` minimizes output for scripts
|
|
56
|
+
| Command | Description |
|
|
57
|
+
| ------------------------------ | ---------------------------------------------------------- |
|
|
58
|
+
| `peakurl login` | Validate and save your PeakURL credentials. |
|
|
59
|
+
| `peakurl whoami` | Show the current authenticated account. |
|
|
60
|
+
| `peakurl create <url>` | Create a new short link. |
|
|
61
|
+
| `peakurl list` | List links in your account. |
|
|
62
|
+
| `peakurl get <id-or-alias>` | Fetch a single link by ID or alias. |
|
|
63
|
+
| `peakurl delete <id-or-alias>` | Delete a link by ID or alias. |
|
|
64
|
+
| `peakurl update` | Show the latest available CLI version and install command. |
|
|
54
65
|
|
|
55
66
|
## Examples
|
|
56
67
|
|
|
57
68
|
Create a short link:
|
|
58
69
|
|
|
59
70
|
```bash
|
|
60
|
-
peakurl create
|
|
71
|
+
peakurl create \
|
|
72
|
+
https://example.com \
|
|
73
|
+
--alias example \
|
|
74
|
+
--title "Example"
|
|
61
75
|
```
|
|
62
76
|
|
|
63
77
|
List links as JSON:
|
|
64
78
|
|
|
65
79
|
```bash
|
|
66
|
-
peakurl list
|
|
80
|
+
peakurl list \
|
|
81
|
+
--limit 10 \
|
|
82
|
+
--json
|
|
67
83
|
```
|
|
68
84
|
|
|
69
85
|
Inspect a link:
|
|
@@ -80,9 +96,40 @@ peakurl delete example
|
|
|
80
96
|
|
|
81
97
|
When `delete` receives an alias or short code, the CLI resolves it to the underlying PeakURL row ID before deleting it.
|
|
82
98
|
|
|
99
|
+
Check the latest available CLI version:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
peakurl update --check
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Show the recommended install command:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
peakurl update
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Install the latest version manually:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm install -g peakurl@latest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Disable update notices in the current shell:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
export PEAKURL_DISABLE_UPDATE_CHECK=1
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Output
|
|
124
|
+
|
|
125
|
+
- Human-readable output is the default
|
|
126
|
+
- `--json` prints machine-readable JSON where supported
|
|
127
|
+
- `--quiet` minimizes output for scripts
|
|
128
|
+
|
|
83
129
|
## Links
|
|
84
130
|
|
|
85
|
-
- Website: https://peakurl.org
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
131
|
+
- Website: <https://peakurl.org/>
|
|
132
|
+
- CLI docs: <https://peakurl.org/docs/cli>
|
|
133
|
+
- API docs: <https://peakurl.org/docs/api>
|
|
134
|
+
- npm package: <https://www.npmjs.com/package/peakurl>
|
|
135
|
+
- Issues: <https://github.com/PeakURL/PeakURL-CLI/issues>
|
package/bin/peakurl.js
CHANGED
|
@@ -13,7 +13,7 @@ var CliError = class extends Error {
|
|
|
13
13
|
this.exitCode = exitCode;
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
-
function
|
|
16
|
+
function wrapCliError(error) {
|
|
17
17
|
if (error instanceof CliError) {
|
|
18
18
|
return error;
|
|
19
19
|
}
|
|
@@ -24,6 +24,14 @@ function toCliError(error) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// src/lib/url.ts
|
|
27
|
+
function validateHttpUrl(parsed, label) {
|
|
28
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
29
|
+
throw new CliError(`${label} must use http or https.`);
|
|
30
|
+
}
|
|
31
|
+
if (parsed.username || parsed.password) {
|
|
32
|
+
throw new CliError(`${label} must not include embedded credentials.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
function normalizeBaseUrl(value) {
|
|
28
36
|
const input = value.trim();
|
|
29
37
|
if (!input) {
|
|
@@ -35,9 +43,7 @@ function normalizeBaseUrl(value) {
|
|
|
35
43
|
} catch {
|
|
36
44
|
throw new CliError(`Invalid base URL: ${value}`);
|
|
37
45
|
}
|
|
38
|
-
|
|
39
|
-
throw new CliError("PeakURL base URLs must use http or https.");
|
|
40
|
-
}
|
|
46
|
+
validateHttpUrl(parsed, "PeakURL base URL");
|
|
41
47
|
parsed.hash = "";
|
|
42
48
|
parsed.search = "";
|
|
43
49
|
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
@@ -62,8 +68,13 @@ function normalizeDestinationUrl(value) {
|
|
|
62
68
|
throw new CliError("A destination URL is required.");
|
|
63
69
|
}
|
|
64
70
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const parsed = new URL(input);
|
|
72
|
+
validateHttpUrl(parsed, "Destination URL");
|
|
73
|
+
return parsed.toString();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof CliError) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
67
78
|
throw new CliError(`Invalid destination URL: ${value}`);
|
|
68
79
|
}
|
|
69
80
|
}
|
|
@@ -234,9 +245,18 @@ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
|
234
245
|
import { dirname, join } from "path";
|
|
235
246
|
import envPaths from "env-paths";
|
|
236
247
|
var CONFIG_FILENAME = "config.json";
|
|
248
|
+
var STATE_FILENAME = "state.json";
|
|
237
249
|
function defaultConfigPath() {
|
|
238
250
|
return join(envPaths("peakurl", { suffix: "" }).config, CONFIG_FILENAME);
|
|
239
251
|
}
|
|
252
|
+
function defaultStatePath() {
|
|
253
|
+
return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
|
|
254
|
+
}
|
|
255
|
+
async function prepareFileDirectory(filePath) {
|
|
256
|
+
const directory = dirname(filePath);
|
|
257
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
258
|
+
return directory;
|
|
259
|
+
}
|
|
240
260
|
var ConfigStore = class {
|
|
241
261
|
filePath;
|
|
242
262
|
/**
|
|
@@ -292,8 +312,7 @@ var ConfigStore = class {
|
|
|
292
312
|
* @param config Normalized credential set to write.
|
|
293
313
|
*/
|
|
294
314
|
async save(config) {
|
|
295
|
-
const directory =
|
|
296
|
-
await mkdir(directory, { recursive: true, mode: 448 });
|
|
315
|
+
const directory = await prepareFileDirectory(this.filePath);
|
|
297
316
|
await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
|
|
298
317
|
`, {
|
|
299
318
|
mode: 384
|
|
@@ -305,6 +324,51 @@ var ConfigStore = class {
|
|
|
305
324
|
}
|
|
306
325
|
}
|
|
307
326
|
};
|
|
327
|
+
var StateStore = class {
|
|
328
|
+
filePath;
|
|
329
|
+
/**
|
|
330
|
+
* Creates a state store bound to one on-disk file.
|
|
331
|
+
*
|
|
332
|
+
* @param filePath Optional override used by tests or advanced callers.
|
|
333
|
+
*/
|
|
334
|
+
constructor(filePath = defaultStatePath()) {
|
|
335
|
+
this.filePath = filePath;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Loads cached state from disk.
|
|
339
|
+
*
|
|
340
|
+
* Missing or invalid files are treated as empty state because the CLI can
|
|
341
|
+
* always recompute update metadata on the next successful network check.
|
|
342
|
+
*
|
|
343
|
+
* @returns Parsed state object or an empty object.
|
|
344
|
+
*/
|
|
345
|
+
async load() {
|
|
346
|
+
try {
|
|
347
|
+
const content = await readFile(this.filePath, "utf8");
|
|
348
|
+
const parsed = JSON.parse(content);
|
|
349
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
350
|
+
} catch {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Persists cached state to disk.
|
|
356
|
+
*
|
|
357
|
+
* @param state State payload to save.
|
|
358
|
+
*/
|
|
359
|
+
async save(state) {
|
|
360
|
+
const directory = await prepareFileDirectory(this.filePath);
|
|
361
|
+
await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
|
|
362
|
+
`, {
|
|
363
|
+
mode: 384
|
|
364
|
+
});
|
|
365
|
+
try {
|
|
366
|
+
await chmod(directory, 448);
|
|
367
|
+
await chmod(this.filePath, 384);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
308
372
|
|
|
309
373
|
// src/lib/auth.ts
|
|
310
374
|
function resolveLoginConfig(input, env) {
|
|
@@ -335,20 +399,126 @@ async function resolveStoredConfig(env, store = new ConfigStore()) {
|
|
|
335
399
|
};
|
|
336
400
|
}
|
|
337
401
|
|
|
402
|
+
// src/lib/output.ts
|
|
403
|
+
function writeStdout(message = "") {
|
|
404
|
+
process.stdout.write(`${message}
|
|
405
|
+
`);
|
|
406
|
+
}
|
|
407
|
+
function writeStderr(message = "") {
|
|
408
|
+
process.stderr.write(`${message}
|
|
409
|
+
`);
|
|
410
|
+
}
|
|
411
|
+
function writeNoticeBox(title, lines, target = "stderr") {
|
|
412
|
+
const contentLines = lines.length > 0 ? lines : [""];
|
|
413
|
+
const width = Math.max(
|
|
414
|
+
title.length,
|
|
415
|
+
...contentLines.map((line) => line.length)
|
|
416
|
+
);
|
|
417
|
+
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
418
|
+
const useTuiBox = stream.isTTY;
|
|
419
|
+
const border = useTuiBox ? {
|
|
420
|
+
topLeft: "\u250C",
|
|
421
|
+
topRight: "\u2510",
|
|
422
|
+
bottomLeft: "\u2514",
|
|
423
|
+
bottomRight: "\u2518",
|
|
424
|
+
horizontal: "\u2500",
|
|
425
|
+
vertical: "\u2502",
|
|
426
|
+
separatorLeft: "\u251C",
|
|
427
|
+
separatorRight: "\u2524"
|
|
428
|
+
} : {
|
|
429
|
+
topLeft: "+",
|
|
430
|
+
topRight: "+",
|
|
431
|
+
bottomLeft: "+",
|
|
432
|
+
bottomRight: "+",
|
|
433
|
+
horizontal: "-",
|
|
434
|
+
vertical: "|",
|
|
435
|
+
separatorLeft: "+",
|
|
436
|
+
separatorRight: "+"
|
|
437
|
+
};
|
|
438
|
+
const topBorder = `${border.topLeft}${border.horizontal.repeat(width + 2)}${border.topRight}`;
|
|
439
|
+
const separator = `${border.separatorLeft}${border.horizontal.repeat(width + 2)}${border.separatorRight}`;
|
|
440
|
+
const bottomBorder = `${border.bottomLeft}${border.horizontal.repeat(width + 2)}${border.bottomRight}`;
|
|
441
|
+
const writeLine = target === "stdout" ? writeStdout : writeStderr;
|
|
442
|
+
writeLine(topBorder);
|
|
443
|
+
writeLine(`${border.vertical} ${title.padEnd(width)} ${border.vertical}`);
|
|
444
|
+
writeLine(separator);
|
|
445
|
+
for (const line of contentLines) {
|
|
446
|
+
writeLine(
|
|
447
|
+
`${border.vertical} ${line.padEnd(width)} ${border.vertical}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
writeLine(bottomBorder);
|
|
451
|
+
}
|
|
452
|
+
function formatTable(headers, rows, target = "stdout") {
|
|
453
|
+
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
454
|
+
const useTuiBox = stream.isTTY;
|
|
455
|
+
const border = useTuiBox ? {
|
|
456
|
+
topLeft: "\u250C",
|
|
457
|
+
topRight: "\u2510",
|
|
458
|
+
bottomLeft: "\u2514",
|
|
459
|
+
bottomRight: "\u2518",
|
|
460
|
+
horizontal: "\u2500",
|
|
461
|
+
vertical: "\u2502",
|
|
462
|
+
separatorLeft: "\u251C",
|
|
463
|
+
separatorRight: "\u2524",
|
|
464
|
+
topJunction: "\u252C",
|
|
465
|
+
middleJunction: "\u253C",
|
|
466
|
+
bottomJunction: "\u2534"
|
|
467
|
+
} : {
|
|
468
|
+
topLeft: "+",
|
|
469
|
+
topRight: "+",
|
|
470
|
+
bottomLeft: "+",
|
|
471
|
+
bottomRight: "+",
|
|
472
|
+
horizontal: "-",
|
|
473
|
+
vertical: "|",
|
|
474
|
+
separatorLeft: "+",
|
|
475
|
+
separatorRight: "+",
|
|
476
|
+
topJunction: "+",
|
|
477
|
+
middleJunction: "+",
|
|
478
|
+
bottomJunction: "+"
|
|
479
|
+
};
|
|
480
|
+
const widths = headers.map(
|
|
481
|
+
(header, index) => Math.max(
|
|
482
|
+
header.length,
|
|
483
|
+
...rows.map((row) => (row[index] ?? "").length)
|
|
484
|
+
)
|
|
485
|
+
);
|
|
486
|
+
const formatTableBorder = (left, join2, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join2)}${right}`;
|
|
487
|
+
const formatTableRow = (cells) => `${border.vertical}${cells.map((cell, index) => ` ${(cell ?? "").padEnd(widths[index])} `).join(border.vertical)}${border.vertical}`;
|
|
488
|
+
return [
|
|
489
|
+
formatTableBorder(border.topLeft, border.topJunction, border.topRight),
|
|
490
|
+
formatTableRow(headers),
|
|
491
|
+
formatTableBorder(
|
|
492
|
+
border.separatorLeft,
|
|
493
|
+
border.middleJunction,
|
|
494
|
+
border.separatorRight
|
|
495
|
+
),
|
|
496
|
+
...rows.map(formatTableRow),
|
|
497
|
+
formatTableBorder(
|
|
498
|
+
border.bottomLeft,
|
|
499
|
+
border.bottomJunction,
|
|
500
|
+
border.bottomRight
|
|
501
|
+
)
|
|
502
|
+
].join("\n");
|
|
503
|
+
}
|
|
504
|
+
function writeJson(value) {
|
|
505
|
+
writeStdout(JSON.stringify(value, null, 2));
|
|
506
|
+
}
|
|
507
|
+
|
|
338
508
|
// src/lib/links.ts
|
|
339
509
|
var LIST_KEYS = ["urls", "items", "results"];
|
|
340
|
-
function
|
|
510
|
+
function objectValue(value) {
|
|
341
511
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
342
512
|
}
|
|
343
|
-
function
|
|
513
|
+
function stringValue(value) {
|
|
344
514
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
345
515
|
}
|
|
346
|
-
function
|
|
516
|
+
function numberValue(value) {
|
|
347
517
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
348
518
|
}
|
|
349
|
-
function
|
|
519
|
+
function pickString(link, keys) {
|
|
350
520
|
for (const key of keys) {
|
|
351
|
-
const value =
|
|
521
|
+
const value = stringValue(link[key]);
|
|
352
522
|
if (value) {
|
|
353
523
|
return value;
|
|
354
524
|
}
|
|
@@ -359,31 +529,31 @@ function truncate(value, maxLength) {
|
|
|
359
529
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
360
530
|
}
|
|
361
531
|
function extractListMeta(data) {
|
|
362
|
-
const record =
|
|
532
|
+
const record = objectValue(data);
|
|
363
533
|
if (!record) {
|
|
364
534
|
return null;
|
|
365
535
|
}
|
|
366
|
-
const meta =
|
|
536
|
+
const meta = objectValue(record.meta);
|
|
367
537
|
if (meta) {
|
|
368
538
|
return {
|
|
369
|
-
page:
|
|
370
|
-
limit:
|
|
371
|
-
totalItems:
|
|
372
|
-
totalPages:
|
|
539
|
+
page: numberValue(meta.page),
|
|
540
|
+
limit: numberValue(meta.limit),
|
|
541
|
+
totalItems: numberValue(meta.totalItems),
|
|
542
|
+
totalPages: numberValue(meta.totalPages)
|
|
373
543
|
};
|
|
374
544
|
}
|
|
375
545
|
return {
|
|
376
|
-
page:
|
|
377
|
-
limit:
|
|
378
|
-
totalItems:
|
|
379
|
-
totalPages:
|
|
546
|
+
page: numberValue(record.page),
|
|
547
|
+
limit: numberValue(record.limit),
|
|
548
|
+
totalItems: numberValue(record.total),
|
|
549
|
+
totalPages: numberValue(record.totalPages)
|
|
380
550
|
};
|
|
381
551
|
}
|
|
382
552
|
function extractLinks(data) {
|
|
383
553
|
if (Array.isArray(data)) {
|
|
384
554
|
return data;
|
|
385
555
|
}
|
|
386
|
-
const record =
|
|
556
|
+
const record = objectValue(data);
|
|
387
557
|
if (record) {
|
|
388
558
|
for (const key of LIST_KEYS) {
|
|
389
559
|
const value = record[key];
|
|
@@ -395,16 +565,16 @@ function extractLinks(data) {
|
|
|
395
565
|
return [];
|
|
396
566
|
}
|
|
397
567
|
function getLinkId(link) {
|
|
398
|
-
return
|
|
568
|
+
return pickString(link, ["id", "_id", "urlId"]) || (link.id !== void 0 ? String(link.id) : void 0);
|
|
399
569
|
}
|
|
400
570
|
function getLinkAlias(link) {
|
|
401
|
-
return
|
|
571
|
+
return pickString(link, ["alias", "shortCode", "slug", "code"]);
|
|
402
572
|
}
|
|
403
573
|
function getLinkShortUrl(link) {
|
|
404
|
-
return
|
|
574
|
+
return pickString(link, ["shortUrl", "shortLink", "shortURL", "url"]);
|
|
405
575
|
}
|
|
406
576
|
function getLinkDestination(link) {
|
|
407
|
-
return
|
|
577
|
+
return pickString(link, [
|
|
408
578
|
"destinationUrl",
|
|
409
579
|
"originalUrl",
|
|
410
580
|
"targetUrl",
|
|
@@ -420,14 +590,14 @@ function formatLinkDetails(link) {
|
|
|
420
590
|
["Alias", getLinkAlias(link)],
|
|
421
591
|
["Short URL", getLinkShortUrl(link)],
|
|
422
592
|
["Destination", getLinkDestination(link)],
|
|
423
|
-
["Title",
|
|
424
|
-
["Status",
|
|
593
|
+
["Title", stringValue(link.title)],
|
|
594
|
+
["Status", stringValue(link.status)],
|
|
425
595
|
[
|
|
426
596
|
"Clicks",
|
|
427
|
-
|
|
597
|
+
numberValue(link.clicks) === void 0 ? void 0 : String(link.clicks)
|
|
428
598
|
],
|
|
429
|
-
["Created",
|
|
430
|
-
["Updated",
|
|
599
|
+
["Created", stringValue(link.createdAt)],
|
|
600
|
+
["Updated", stringValue(link.updatedAt)]
|
|
431
601
|
].filter((entry) => Boolean(entry[1]));
|
|
432
602
|
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
433
603
|
}
|
|
@@ -435,23 +605,15 @@ function formatLinksTable(links) {
|
|
|
435
605
|
if (links.length === 0) {
|
|
436
606
|
return "No links found.";
|
|
437
607
|
}
|
|
438
|
-
const headers = ["ID", "
|
|
608
|
+
const headers = ["ID", "Alias", "Short URL", "Destination", "Status"];
|
|
439
609
|
const rows = links.map((link) => [
|
|
440
610
|
truncate(getLinkId(link) || "-", 18),
|
|
441
|
-
truncate(getLinkAlias(link) || "-",
|
|
611
|
+
truncate(getLinkAlias(link) || "-", 12),
|
|
442
612
|
truncate(getLinkShortUrl(link) || "-", 36),
|
|
443
613
|
truncate(getLinkDestination(link) || "-", 52),
|
|
444
|
-
truncate(
|
|
614
|
+
truncate(stringValue(link.status) || "-", 12)
|
|
445
615
|
]);
|
|
446
|
-
|
|
447
|
-
(header, index) => Math.max(header.length, ...rows.map((row) => row[index].length))
|
|
448
|
-
);
|
|
449
|
-
const renderRow = (row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
|
450
|
-
return [
|
|
451
|
-
renderRow(headers),
|
|
452
|
-
renderRow(widths.map((width) => "-".repeat(width))),
|
|
453
|
-
...rows.map(renderRow)
|
|
454
|
-
].join("\n");
|
|
616
|
+
return formatTable(headers, rows);
|
|
455
617
|
}
|
|
456
618
|
function formatListSummary(data, count) {
|
|
457
619
|
const meta = extractListMeta(data);
|
|
@@ -467,19 +629,6 @@ function formatListSummary(data, count) {
|
|
|
467
629
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
468
630
|
}
|
|
469
631
|
|
|
470
|
-
// src/lib/output.ts
|
|
471
|
-
function writeStdout(message = "") {
|
|
472
|
-
process.stdout.write(`${message}
|
|
473
|
-
`);
|
|
474
|
-
}
|
|
475
|
-
function writeStderr(message = "") {
|
|
476
|
-
process.stderr.write(`${message}
|
|
477
|
-
`);
|
|
478
|
-
}
|
|
479
|
-
function writeJson(value) {
|
|
480
|
-
writeStdout(JSON.stringify(value, null, 2));
|
|
481
|
-
}
|
|
482
|
-
|
|
483
632
|
// src/commands/create.ts
|
|
484
633
|
function normalizeExpiresAt(value) {
|
|
485
634
|
if (!value) {
|
|
@@ -585,22 +734,22 @@ async function listCommand(options) {
|
|
|
585
734
|
}
|
|
586
735
|
|
|
587
736
|
// src/lib/users.ts
|
|
588
|
-
function
|
|
737
|
+
function stringValue2(value) {
|
|
589
738
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
590
739
|
}
|
|
591
740
|
function getUserLabel(user) {
|
|
592
|
-
const fullName = [
|
|
593
|
-
return fullName ||
|
|
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");
|
|
594
743
|
}
|
|
595
744
|
function getQuietUserValue(user) {
|
|
596
|
-
return
|
|
745
|
+
return stringValue2(user.username) || stringValue2(user.email) || String(user.id ?? "");
|
|
597
746
|
}
|
|
598
747
|
function formatUserDetails(user) {
|
|
599
748
|
const lines = [
|
|
600
749
|
["Name", getUserLabel(user)],
|
|
601
|
-
["Username",
|
|
602
|
-
["Email",
|
|
603
|
-
["Role",
|
|
750
|
+
["Username", stringValue2(user.username)],
|
|
751
|
+
["Email", stringValue2(user.email)],
|
|
752
|
+
["Role", stringValue2(user.role)],
|
|
604
753
|
["ID", user.id === void 0 ? void 0 : String(user.id)]
|
|
605
754
|
].filter((entry) => Boolean(entry[1]));
|
|
606
755
|
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
@@ -612,7 +761,7 @@ async function loginCommand(options) {
|
|
|
612
761
|
const client = new PeakUrlApiClient(credentials);
|
|
613
762
|
const response = await client.whoami();
|
|
614
763
|
await new ConfigStore().save(credentials);
|
|
615
|
-
const
|
|
764
|
+
const responseBody = {
|
|
616
765
|
success: true,
|
|
617
766
|
message: `Saved credentials for ${credentials.baseUrl}.`,
|
|
618
767
|
data: {
|
|
@@ -622,7 +771,7 @@ async function loginCommand(options) {
|
|
|
622
771
|
timestamp: response.timestamp
|
|
623
772
|
};
|
|
624
773
|
if (options.json) {
|
|
625
|
-
writeJson(
|
|
774
|
+
writeJson(responseBody);
|
|
626
775
|
return;
|
|
627
776
|
}
|
|
628
777
|
if (options.quiet) {
|
|
@@ -633,6 +782,256 @@ async function loginCommand(options) {
|
|
|
633
782
|
writeStdout(formatUserDetails(response.data));
|
|
634
783
|
}
|
|
635
784
|
|
|
785
|
+
// src/lib/update.ts
|
|
786
|
+
var PACKAGE_NAME = "peakurl";
|
|
787
|
+
var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
788
|
+
var CACHE_TTL_MS = 1e3 * 60 * 60 * 12;
|
|
789
|
+
var NOTICE_TTL_MS = 1e3 * 60 * 60 * 24;
|
|
790
|
+
function parseTime(value) {
|
|
791
|
+
if (!value) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
const parsed = Date.parse(value);
|
|
795
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
796
|
+
}
|
|
797
|
+
function getRegistryBaseUrl(env) {
|
|
798
|
+
const candidate = env.PEAKURL_NPM_REGISTRY_URL?.trim();
|
|
799
|
+
if (!candidate) {
|
|
800
|
+
return DEFAULT_REGISTRY_URL;
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
const parsed = new URL(candidate);
|
|
804
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password) {
|
|
805
|
+
return DEFAULT_REGISTRY_URL;
|
|
806
|
+
}
|
|
807
|
+
return candidate;
|
|
808
|
+
} catch {
|
|
809
|
+
return DEFAULT_REGISTRY_URL;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function normalizeRegistryUrl(registryUrl) {
|
|
813
|
+
return registryUrl.replace(/\/+$/, "");
|
|
814
|
+
}
|
|
815
|
+
function parseSemver(version) {
|
|
816
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
817
|
+
version
|
|
818
|
+
);
|
|
819
|
+
if (!match) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
major: Number.parseInt(match[1], 10),
|
|
824
|
+
minor: Number.parseInt(match[2], 10),
|
|
825
|
+
patch: Number.parseInt(match[3], 10),
|
|
826
|
+
prerelease: match[4] ? match[4].split(".") : []
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function comparePrerelease(left, right) {
|
|
830
|
+
if (left.length === 0 && right.length === 0) {
|
|
831
|
+
return 0;
|
|
832
|
+
}
|
|
833
|
+
if (left.length === 0) {
|
|
834
|
+
return 1;
|
|
835
|
+
}
|
|
836
|
+
if (right.length === 0) {
|
|
837
|
+
return -1;
|
|
838
|
+
}
|
|
839
|
+
const length = Math.max(left.length, right.length);
|
|
840
|
+
for (let index = 0; index < length; index += 1) {
|
|
841
|
+
const leftPart = left[index];
|
|
842
|
+
const rightPart = right[index];
|
|
843
|
+
if (leftPart === void 0) {
|
|
844
|
+
return -1;
|
|
845
|
+
}
|
|
846
|
+
if (rightPart === void 0) {
|
|
847
|
+
return 1;
|
|
848
|
+
}
|
|
849
|
+
const leftNumber = /^\d+$/.test(leftPart) ? Number.parseInt(leftPart, 10) : null;
|
|
850
|
+
const rightNumber = /^\d+$/.test(rightPart) ? Number.parseInt(rightPart, 10) : null;
|
|
851
|
+
if (leftNumber !== null && rightNumber !== null) {
|
|
852
|
+
if (leftNumber !== rightNumber) {
|
|
853
|
+
return leftNumber > rightNumber ? 1 : -1;
|
|
854
|
+
}
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (leftNumber !== null) {
|
|
858
|
+
return -1;
|
|
859
|
+
}
|
|
860
|
+
if (rightNumber !== null) {
|
|
861
|
+
return 1;
|
|
862
|
+
}
|
|
863
|
+
if (leftPart !== rightPart) {
|
|
864
|
+
return leftPart > rightPart ? 1 : -1;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
function compareSemver(left, right) {
|
|
870
|
+
const parsedLeft = parseSemver(left);
|
|
871
|
+
const parsedRight = parseSemver(right);
|
|
872
|
+
if (!parsedLeft || !parsedRight) {
|
|
873
|
+
return left.localeCompare(right, void 0, { numeric: true });
|
|
874
|
+
}
|
|
875
|
+
if (parsedLeft.major !== parsedRight.major) {
|
|
876
|
+
return parsedLeft.major > parsedRight.major ? 1 : -1;
|
|
877
|
+
}
|
|
878
|
+
if (parsedLeft.minor !== parsedRight.minor) {
|
|
879
|
+
return parsedLeft.minor > parsedRight.minor ? 1 : -1;
|
|
880
|
+
}
|
|
881
|
+
if (parsedLeft.patch !== parsedRight.patch) {
|
|
882
|
+
return parsedLeft.patch > parsedRight.patch ? 1 : -1;
|
|
883
|
+
}
|
|
884
|
+
return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
|
|
885
|
+
}
|
|
886
|
+
function getUpdateInstallCommand() {
|
|
887
|
+
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
888
|
+
}
|
|
889
|
+
async function fetchLatestPackageVersion(env) {
|
|
890
|
+
const registryUrl = normalizeRegistryUrl(getRegistryBaseUrl(env));
|
|
891
|
+
const url = `${registryUrl}/${PACKAGE_NAME}/latest`;
|
|
892
|
+
const controller = new AbortController();
|
|
893
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
894
|
+
try {
|
|
895
|
+
const response = await fetch(url, {
|
|
896
|
+
headers: {
|
|
897
|
+
accept: "application/json"
|
|
898
|
+
},
|
|
899
|
+
signal: controller.signal
|
|
900
|
+
});
|
|
901
|
+
if (!response.ok) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
const payload = await response.json();
|
|
905
|
+
return typeof payload.version === "string" ? payload.version : null;
|
|
906
|
+
} catch {
|
|
907
|
+
return null;
|
|
908
|
+
} finally {
|
|
909
|
+
clearTimeout(timeout);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async function getCachedUpdateState(store) {
|
|
913
|
+
const state = await store.load();
|
|
914
|
+
return state.update ?? {};
|
|
915
|
+
}
|
|
916
|
+
async function saveUpdateState(store, update) {
|
|
917
|
+
const state = await store.load();
|
|
918
|
+
await store.save({
|
|
919
|
+
...state,
|
|
920
|
+
update
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
async function getLatestPackageVersion(env, options) {
|
|
924
|
+
const store = options?.store ?? new StateStore();
|
|
925
|
+
const updateState = await getCachedUpdateState(store);
|
|
926
|
+
const lastCheckedAt = parseTime(updateState.lastCheckedAt);
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
if (!options?.forceRefresh && updateState.latestVersion && lastCheckedAt !== null && now - lastCheckedAt < CACHE_TTL_MS) {
|
|
929
|
+
return updateState.latestVersion;
|
|
930
|
+
}
|
|
931
|
+
const latestVersion = await fetchLatestPackageVersion(env);
|
|
932
|
+
if (!latestVersion) {
|
|
933
|
+
return updateState.latestVersion ?? null;
|
|
934
|
+
}
|
|
935
|
+
await saveUpdateState(store, {
|
|
936
|
+
...updateState,
|
|
937
|
+
latestVersion,
|
|
938
|
+
lastCheckedAt: new Date(now).toISOString()
|
|
939
|
+
});
|
|
940
|
+
return latestVersion;
|
|
941
|
+
}
|
|
942
|
+
async function getUpdateStatus(currentVersion, env, options) {
|
|
943
|
+
const resolvedLatestVersion = await getLatestPackageVersion(env, {
|
|
944
|
+
forceRefresh: options?.forceRefresh,
|
|
945
|
+
store: options?.store
|
|
946
|
+
});
|
|
947
|
+
if (!resolvedLatestVersion && options?.forceRefresh) {
|
|
948
|
+
throw new CliError(
|
|
949
|
+
`Could not reach the npm registry to check the latest ${PACKAGE_NAME} version.`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
const latestVersion = resolvedLatestVersion ?? currentVersion;
|
|
953
|
+
return {
|
|
954
|
+
currentVersion,
|
|
955
|
+
latestVersion,
|
|
956
|
+
isOutdated: compareSemver(currentVersion, latestVersion) < 0,
|
|
957
|
+
installCommand: getUpdateInstallCommand()
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function writeUpdateNotice(status) {
|
|
961
|
+
writeNoticeBox("Update Available", [
|
|
962
|
+
`${PACKAGE_NAME} ${status.currentVersion} -> ${status.latestVersion}`,
|
|
963
|
+
`Run: ${status.installCommand}`
|
|
964
|
+
]);
|
|
965
|
+
}
|
|
966
|
+
async function maybeShowUpdateNotice(options) {
|
|
967
|
+
if (options.env.PEAKURL_DISABLE_UPDATE_CHECK === "1" || options.commandName === "update" || options.options?.json || options.options?.quiet) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (options.env.PEAKURL_FORCE_UPDATE_NOTICE !== "1" && !process.stderr.isTTY) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const store = new StateStore();
|
|
974
|
+
const updateState = await getCachedUpdateState(store);
|
|
975
|
+
const status = await getUpdateStatus(options.currentVersion, options.env, {
|
|
976
|
+
store
|
|
977
|
+
});
|
|
978
|
+
if (!status.isOutdated) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const lastNotifiedAt = parseTime(updateState.lastNotifiedAt);
|
|
982
|
+
const alreadyNotifiedForVersion = updateState.lastNotifiedVersion === status.latestVersion;
|
|
983
|
+
if (alreadyNotifiedForVersion && lastNotifiedAt !== null && Date.now() - lastNotifiedAt < NOTICE_TTL_MS) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
await saveUpdateState(store, {
|
|
987
|
+
...updateState,
|
|
988
|
+
lastNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
989
|
+
lastNotifiedVersion: status.latestVersion
|
|
990
|
+
});
|
|
991
|
+
writeUpdateNotice(status);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/commands/update.ts
|
|
995
|
+
async function updateCommand(options, currentVersion) {
|
|
996
|
+
const status = await getUpdateStatus(currentVersion, process.env, {
|
|
997
|
+
forceRefresh: true
|
|
998
|
+
});
|
|
999
|
+
const responseBody = {
|
|
1000
|
+
success: true,
|
|
1001
|
+
message: status.isOutdated ? `A newer PeakURL CLI version is available (${status.latestVersion}).` : `PeakURL CLI ${status.currentVersion} is up to date.`,
|
|
1002
|
+
data: {
|
|
1003
|
+
currentVersion: status.currentVersion,
|
|
1004
|
+
latestVersion: status.latestVersion,
|
|
1005
|
+
isOutdated: status.isOutdated,
|
|
1006
|
+
installCommand: status.installCommand,
|
|
1007
|
+
checkOnly: Boolean(options.check)
|
|
1008
|
+
},
|
|
1009
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1010
|
+
};
|
|
1011
|
+
if (options.json) {
|
|
1012
|
+
writeJson(responseBody);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (!status.isOutdated) {
|
|
1016
|
+
if (!options.quiet) {
|
|
1017
|
+
writeStdout(`PeakURL CLI ${status.currentVersion} is up to date.`);
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (options.quiet) {
|
|
1022
|
+
writeStdout(status.installCommand);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
writeNoticeBox(
|
|
1026
|
+
"Update Available",
|
|
1027
|
+
[
|
|
1028
|
+
`peakurl ${status.currentVersion} -> ${status.latestVersion}`,
|
|
1029
|
+
`Run: ${status.installCommand}`
|
|
1030
|
+
],
|
|
1031
|
+
"stdout"
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
636
1035
|
// src/commands/whoami.ts
|
|
637
1036
|
async function whoamiCommand(options) {
|
|
638
1037
|
const config = await resolveStoredConfig(process.env);
|
|
@@ -661,7 +1060,7 @@ function parsePositiveInteger(label) {
|
|
|
661
1060
|
return parsed;
|
|
662
1061
|
};
|
|
663
1062
|
}
|
|
664
|
-
async function
|
|
1063
|
+
async function getCliVersion() {
|
|
665
1064
|
const packageJson = new URL("../package.json", import.meta.url);
|
|
666
1065
|
const content = await readFile2(packageJson, "utf8");
|
|
667
1066
|
const parsed = JSON.parse(content);
|
|
@@ -669,7 +1068,8 @@ async function readVersion() {
|
|
|
669
1068
|
}
|
|
670
1069
|
async function main() {
|
|
671
1070
|
const program = new Command();
|
|
672
|
-
|
|
1071
|
+
const version = await getCliVersion();
|
|
1072
|
+
program.name("peakurl").description("PeakURL command-line interface").version(version).showHelpAfterError().showSuggestionAfterError().addHelpText(
|
|
673
1073
|
"after",
|
|
674
1074
|
`
|
|
675
1075
|
Examples:
|
|
@@ -677,9 +1077,19 @@ Examples:
|
|
|
677
1077
|
peakurl whoami --json
|
|
678
1078
|
peakurl create https://example.com --alias example
|
|
679
1079
|
peakurl list --limit 10
|
|
1080
|
+
peakurl update --check
|
|
680
1081
|
peakurl get example
|
|
681
1082
|
peakurl delete example --quiet`
|
|
682
1083
|
).exitOverride();
|
|
1084
|
+
program.hook("preAction", async (_command, actionCommand) => {
|
|
1085
|
+
const options = actionCommand.optsWithGlobals();
|
|
1086
|
+
await maybeShowUpdateNotice({
|
|
1087
|
+
currentVersion: version,
|
|
1088
|
+
commandName: actionCommand.name(),
|
|
1089
|
+
options,
|
|
1090
|
+
env: process.env
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
683
1093
|
program.command("login").description(
|
|
684
1094
|
"Save PeakURL credentials after verifying them with GET /users/me."
|
|
685
1095
|
).option(
|
|
@@ -694,13 +1104,19 @@ Examples:
|
|
|
694
1104
|
program.command("list").description("List PeakURL short links.").option("--page <number>", "Page number", parsePositiveInteger("page")).option("--limit <number>", "Page size", parsePositiveInteger("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);
|
|
695
1105
|
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);
|
|
696
1106
|
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
|
+
program.command("update").description(
|
|
1108
|
+
"Check for a newer CLI version and print the npm command to install it."
|
|
1109
|
+
).option(
|
|
1110
|
+
"--check",
|
|
1111
|
+
"Alias for checking update status without changing anything"
|
|
1112
|
+
).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action((options) => updateCommand(options, version));
|
|
697
1113
|
try {
|
|
698
1114
|
await program.parseAsync(process.argv);
|
|
699
1115
|
} catch (error) {
|
|
700
1116
|
if (error instanceof CommanderError) {
|
|
701
1117
|
process.exit(error.exitCode);
|
|
702
1118
|
}
|
|
703
|
-
const cliError =
|
|
1119
|
+
const cliError = wrapCliError(error);
|
|
704
1120
|
writeStderr(cliError.message);
|
|
705
1121
|
process.exit(cliError.exitCode);
|
|
706
1122
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peakurl",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "PeakURL
|
|
5
|
-
"homepage": "https://peakurl.org
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Official CLI for creating, listing, and managing PeakURL short links from the terminal",
|
|
5
|
+
"homepage": "https://peakurl.org",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/PeakURL/PeakURL-CLI/issues"
|
|
8
|
+
},
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
8
11
|
"url": "git+https://github.com/PeakURL/PeakURL-CLI.git"
|
|
9
12
|
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"peakurl",
|
|
15
|
+
"url shortener",
|
|
16
|
+
"short links",
|
|
17
|
+
"link shortening",
|
|
18
|
+
"cli",
|
|
19
|
+
"command line",
|
|
20
|
+
"terminal",
|
|
21
|
+
"productivity"
|
|
22
|
+
],
|
|
10
23
|
"type": "module",
|
|
11
24
|
"bin": {
|
|
12
25
|
"peakurl": "bin/peakurl.js"
|
|
@@ -22,7 +35,7 @@
|
|
|
22
35
|
"dev": "tsx src/index.ts",
|
|
23
36
|
"format": "prettier --write .",
|
|
24
37
|
"format:check": "prettier --check .",
|
|
25
|
-
"test": "tsx --test test/*.test.ts",
|
|
38
|
+
"test": "npm run build && tsx --test test/*.test.ts",
|
|
26
39
|
"typecheck": "tsc --noEmit",
|
|
27
40
|
"prepare": "npm run build"
|
|
28
41
|
},
|