peakurl 0.1.1 → 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/LICENSE +21 -0
- package/README.md +13 -1
- package/bin/peakurl.js +453 -311
- package/package.json +1 -1
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,9 +1,11 @@
|
|
|
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
6
|
|
|
7
|
+
Learn more in the full CLI docs: <https://peakurl.org/docs/cli>
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
Node.js 20 or later is required.
|
|
@@ -32,6 +34,7 @@ peakurl create \
|
|
|
32
34
|
|
|
33
35
|
peakurl list
|
|
34
36
|
peakurl whoami
|
|
37
|
+
peakurl logout
|
|
35
38
|
```
|
|
36
39
|
|
|
37
40
|
## Authentication
|
|
@@ -41,6 +44,7 @@ The CLI uses PeakURL bearer API keys and validates them against `GET /api/v1/use
|
|
|
41
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`
|
|
42
45
|
- API keys are opaque 48-character hex tokens
|
|
43
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
|
|
44
48
|
|
|
45
49
|
For CI or automation, you can also authenticate with environment variables:
|
|
46
50
|
|
|
@@ -55,6 +59,7 @@ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
|
|
|
55
59
|
| ------------------------------ | ---------------------------------------------------------- |
|
|
56
60
|
| `peakurl login` | Validate and save your PeakURL credentials. |
|
|
57
61
|
| `peakurl whoami` | Show the current authenticated account. |
|
|
62
|
+
| `peakurl logout` | Remove saved local CLI credentials. |
|
|
58
63
|
| `peakurl create <url>` | Create a new short link. |
|
|
59
64
|
| `peakurl list` | List links in your account. |
|
|
60
65
|
| `peakurl get <id-or-alias>` | Fetch a single link by ID or alias. |
|
|
@@ -86,6 +91,12 @@ Inspect a link:
|
|
|
86
91
|
peakurl get example
|
|
87
92
|
```
|
|
88
93
|
|
|
94
|
+
Log out from saved local credentials:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
peakurl logout
|
|
98
|
+
```
|
|
99
|
+
|
|
89
100
|
Delete a link:
|
|
90
101
|
|
|
91
102
|
```bash
|
|
@@ -127,6 +138,7 @@ export PEAKURL_DISABLE_UPDATE_CHECK=1
|
|
|
127
138
|
## Links
|
|
128
139
|
|
|
129
140
|
- Website: <https://peakurl.org/>
|
|
141
|
+
- CLI docs: <https://peakurl.org/docs/cli>
|
|
130
142
|
- API docs: <https://peakurl.org/docs/api>
|
|
131
143
|
- npm package: <https://www.npmjs.com/package/peakurl>
|
|
132
144
|
- Issues: <https://github.com/PeakURL/PeakURL-CLI/issues>
|
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
|
}
|
|
@@ -24,6 +26,14 @@ function toCliError(error) {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// src/lib/url.ts
|
|
29
|
+
function validateHttpUrl(parsed, label) {
|
|
30
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
31
|
+
throw new CliError(`${label} must use http or https.`);
|
|
32
|
+
}
|
|
33
|
+
if (parsed.username || parsed.password) {
|
|
34
|
+
throw new CliError(`${label} must not include embedded credentials.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
27
37
|
function normalizeBaseUrl(value) {
|
|
28
38
|
const input = value.trim();
|
|
29
39
|
if (!input) {
|
|
@@ -35,9 +45,7 @@ function normalizeBaseUrl(value) {
|
|
|
35
45
|
} catch {
|
|
36
46
|
throw new CliError(`Invalid base URL: ${value}`);
|
|
37
47
|
}
|
|
38
|
-
|
|
39
|
-
throw new CliError("PeakURL base URLs must use http or https.");
|
|
40
|
-
}
|
|
48
|
+
validateHttpUrl(parsed, "PeakURL base URL");
|
|
41
49
|
parsed.hash = "";
|
|
42
50
|
parsed.search = "";
|
|
43
51
|
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
@@ -62,25 +70,30 @@ function normalizeDestinationUrl(value) {
|
|
|
62
70
|
throw new CliError("A destination URL is required.");
|
|
63
71
|
}
|
|
64
72
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
const parsed = new URL(input);
|
|
74
|
+
validateHttpUrl(parsed, "Destination URL");
|
|
75
|
+
return parsed.toString();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof CliError) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
67
80
|
throw new CliError(`Invalid destination URL: ${value}`);
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
// src/api/client.ts
|
|
72
|
-
function
|
|
85
|
+
function isApiResponse(value) {
|
|
73
86
|
return Boolean(
|
|
74
87
|
value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
|
|
75
88
|
);
|
|
76
89
|
}
|
|
77
|
-
function
|
|
90
|
+
function networkError(baseUrl, error) {
|
|
78
91
|
if (error instanceof Error && error.message) {
|
|
79
92
|
return `Could not reach PeakURL at ${baseUrl}. ${error.message}`;
|
|
80
93
|
}
|
|
81
94
|
return `Could not reach PeakURL at ${baseUrl}.`;
|
|
82
95
|
}
|
|
83
|
-
var
|
|
96
|
+
var ApiClient = class {
|
|
84
97
|
/**
|
|
85
98
|
* Creates a client bound to one resolved credential set.
|
|
86
99
|
*
|
|
@@ -120,12 +133,7 @@ var PeakUrlApiClient = class {
|
|
|
120
133
|
* @returns API response envelope containing list data.
|
|
121
134
|
*/
|
|
122
135
|
listUrls(query) {
|
|
123
|
-
return this.request(
|
|
124
|
-
"GET",
|
|
125
|
-
"urls",
|
|
126
|
-
void 0,
|
|
127
|
-
query
|
|
128
|
-
);
|
|
136
|
+
return this.request("GET", "urls", void 0, query);
|
|
129
137
|
}
|
|
130
138
|
/**
|
|
131
139
|
* Loads a single short URL by identifier or alias.
|
|
@@ -180,13 +188,9 @@ var PeakUrlApiClient = class {
|
|
|
180
188
|
body: body ? JSON.stringify(body) : void 0
|
|
181
189
|
});
|
|
182
190
|
} catch (error) {
|
|
183
|
-
throw new CliError(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
cause: error instanceof Error ? error : void 0
|
|
188
|
-
}
|
|
189
|
-
);
|
|
191
|
+
throw new CliError(networkError(this.config.baseUrl, error), 1, {
|
|
192
|
+
cause: error instanceof Error ? error : void 0
|
|
193
|
+
});
|
|
190
194
|
}
|
|
191
195
|
const rawText = await response.text();
|
|
192
196
|
if (!rawText) {
|
|
@@ -213,7 +217,7 @@ var PeakUrlApiClient = class {
|
|
|
213
217
|
}
|
|
214
218
|
throw new CliError("PeakURL returned an invalid JSON response.");
|
|
215
219
|
}
|
|
216
|
-
if (!
|
|
220
|
+
if (!isApiResponse(parsed)) {
|
|
217
221
|
throw new CliError(
|
|
218
222
|
"PeakURL returned an unexpected response envelope."
|
|
219
223
|
);
|
|
@@ -230,18 +234,18 @@ var PeakUrlApiClient = class {
|
|
|
230
234
|
};
|
|
231
235
|
|
|
232
236
|
// src/config/store.ts
|
|
233
|
-
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
237
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
234
238
|
import { dirname, join } from "path";
|
|
235
239
|
import envPaths from "env-paths";
|
|
236
240
|
var CONFIG_FILENAME = "config.json";
|
|
237
241
|
var STATE_FILENAME = "state.json";
|
|
238
|
-
function
|
|
242
|
+
function getConfigPath() {
|
|
239
243
|
return join(envPaths("peakurl", { suffix: "" }).config, CONFIG_FILENAME);
|
|
240
244
|
}
|
|
241
|
-
function
|
|
245
|
+
function getStatePath() {
|
|
242
246
|
return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
|
|
243
247
|
}
|
|
244
|
-
async function
|
|
248
|
+
async function ensureParentDir(filePath) {
|
|
245
249
|
const directory = dirname(filePath);
|
|
246
250
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
247
251
|
return directory;
|
|
@@ -253,7 +257,7 @@ var ConfigStore = class {
|
|
|
253
257
|
*
|
|
254
258
|
* @param filePath Optional override used by tests or advanced callers.
|
|
255
259
|
*/
|
|
256
|
-
constructor(filePath =
|
|
260
|
+
constructor(filePath = getConfigPath()) {
|
|
257
261
|
this.filePath = filePath;
|
|
258
262
|
}
|
|
259
263
|
/**
|
|
@@ -301,7 +305,7 @@ var ConfigStore = class {
|
|
|
301
305
|
* @param config Normalized credential set to write.
|
|
302
306
|
*/
|
|
303
307
|
async save(config) {
|
|
304
|
-
const directory = await
|
|
308
|
+
const directory = await ensureParentDir(this.filePath);
|
|
305
309
|
await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
|
|
306
310
|
`, {
|
|
307
311
|
mode: 384
|
|
@@ -312,6 +316,29 @@ var ConfigStore = class {
|
|
|
312
316
|
} catch {
|
|
313
317
|
}
|
|
314
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
|
+
}
|
|
315
342
|
};
|
|
316
343
|
var StateStore = class {
|
|
317
344
|
filePath;
|
|
@@ -320,7 +347,7 @@ var StateStore = class {
|
|
|
320
347
|
*
|
|
321
348
|
* @param filePath Optional override used by tests or advanced callers.
|
|
322
349
|
*/
|
|
323
|
-
constructor(filePath =
|
|
350
|
+
constructor(filePath = getStatePath()) {
|
|
324
351
|
this.filePath = filePath;
|
|
325
352
|
}
|
|
326
353
|
/**
|
|
@@ -346,16 +373,30 @@ var StateStore = class {
|
|
|
346
373
|
* @param state State payload to save.
|
|
347
374
|
*/
|
|
348
375
|
async save(state) {
|
|
349
|
-
await
|
|
376
|
+
const directory = await ensureParentDir(this.filePath);
|
|
350
377
|
await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
|
|
351
378
|
`, {
|
|
352
379
|
mode: 384
|
|
353
380
|
});
|
|
381
|
+
try {
|
|
382
|
+
await chmod(directory, 448);
|
|
383
|
+
await chmod(this.filePath, 384);
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
354
386
|
}
|
|
355
387
|
};
|
|
356
388
|
|
|
357
389
|
// src/lib/auth.ts
|
|
358
|
-
|
|
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) {
|
|
359
400
|
const baseUrl = input.baseUrl?.trim() || env.PEAKURL_BASE_URL?.trim();
|
|
360
401
|
const apiKey = input.apiKey?.trim() || env.PEAKURL_API_KEY?.trim();
|
|
361
402
|
if (!baseUrl || !apiKey) {
|
|
@@ -368,14 +409,14 @@ function resolveLoginConfig(input, env) {
|
|
|
368
409
|
apiKey
|
|
369
410
|
};
|
|
370
411
|
}
|
|
371
|
-
async function
|
|
412
|
+
async function getAuthConfig(env, store = new ConfigStore()) {
|
|
372
413
|
const saved = await store.load();
|
|
373
414
|
const baseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.baseUrl;
|
|
374
415
|
const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
|
|
375
416
|
if (!baseUrl || !apiKey) {
|
|
376
|
-
throw new CliError(
|
|
377
|
-
|
|
378
|
-
);
|
|
417
|
+
throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
|
|
418
|
+
kind: "auth_required"
|
|
419
|
+
});
|
|
379
420
|
}
|
|
380
421
|
return {
|
|
381
422
|
baseUrl: normalizeBaseUrl(baseUrl),
|
|
@@ -383,20 +424,126 @@ async function resolveStoredConfig(env, store = new ConfigStore()) {
|
|
|
383
424
|
};
|
|
384
425
|
}
|
|
385
426
|
|
|
427
|
+
// src/lib/output.ts
|
|
428
|
+
function writeStdout(message = "") {
|
|
429
|
+
process.stdout.write(`${message}
|
|
430
|
+
`);
|
|
431
|
+
}
|
|
432
|
+
function writeStderr(message = "") {
|
|
433
|
+
process.stderr.write(`${message}
|
|
434
|
+
`);
|
|
435
|
+
}
|
|
436
|
+
function writeNoticeBox(title, lines, target = "stderr") {
|
|
437
|
+
const contentLines = lines.length > 0 ? lines : [""];
|
|
438
|
+
const width = Math.max(
|
|
439
|
+
title.length,
|
|
440
|
+
...contentLines.map((line) => line.length)
|
|
441
|
+
);
|
|
442
|
+
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
443
|
+
const useTuiBox = stream.isTTY;
|
|
444
|
+
const border = useTuiBox ? {
|
|
445
|
+
topLeft: "\u250C",
|
|
446
|
+
topRight: "\u2510",
|
|
447
|
+
bottomLeft: "\u2514",
|
|
448
|
+
bottomRight: "\u2518",
|
|
449
|
+
horizontal: "\u2500",
|
|
450
|
+
vertical: "\u2502",
|
|
451
|
+
separatorLeft: "\u251C",
|
|
452
|
+
separatorRight: "\u2524"
|
|
453
|
+
} : {
|
|
454
|
+
topLeft: "+",
|
|
455
|
+
topRight: "+",
|
|
456
|
+
bottomLeft: "+",
|
|
457
|
+
bottomRight: "+",
|
|
458
|
+
horizontal: "-",
|
|
459
|
+
vertical: "|",
|
|
460
|
+
separatorLeft: "+",
|
|
461
|
+
separatorRight: "+"
|
|
462
|
+
};
|
|
463
|
+
const topBorder = `${border.topLeft}${border.horizontal.repeat(width + 2)}${border.topRight}`;
|
|
464
|
+
const separator = `${border.separatorLeft}${border.horizontal.repeat(width + 2)}${border.separatorRight}`;
|
|
465
|
+
const bottomBorder = `${border.bottomLeft}${border.horizontal.repeat(width + 2)}${border.bottomRight}`;
|
|
466
|
+
const writeLine = target === "stdout" ? writeStdout : writeStderr;
|
|
467
|
+
writeLine(topBorder);
|
|
468
|
+
writeLine(`${border.vertical} ${title.padEnd(width)} ${border.vertical}`);
|
|
469
|
+
writeLine(separator);
|
|
470
|
+
for (const line of contentLines) {
|
|
471
|
+
writeLine(
|
|
472
|
+
`${border.vertical} ${line.padEnd(width)} ${border.vertical}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
writeLine(bottomBorder);
|
|
476
|
+
}
|
|
477
|
+
function formatTable(headers, rows, target = "stdout") {
|
|
478
|
+
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
479
|
+
const useTuiBox = stream.isTTY;
|
|
480
|
+
const border = useTuiBox ? {
|
|
481
|
+
topLeft: "\u250C",
|
|
482
|
+
topRight: "\u2510",
|
|
483
|
+
bottomLeft: "\u2514",
|
|
484
|
+
bottomRight: "\u2518",
|
|
485
|
+
horizontal: "\u2500",
|
|
486
|
+
vertical: "\u2502",
|
|
487
|
+
separatorLeft: "\u251C",
|
|
488
|
+
separatorRight: "\u2524",
|
|
489
|
+
topJunction: "\u252C",
|
|
490
|
+
middleJunction: "\u253C",
|
|
491
|
+
bottomJunction: "\u2534"
|
|
492
|
+
} : {
|
|
493
|
+
topLeft: "+",
|
|
494
|
+
topRight: "+",
|
|
495
|
+
bottomLeft: "+",
|
|
496
|
+
bottomRight: "+",
|
|
497
|
+
horizontal: "-",
|
|
498
|
+
vertical: "|",
|
|
499
|
+
separatorLeft: "+",
|
|
500
|
+
separatorRight: "+",
|
|
501
|
+
topJunction: "+",
|
|
502
|
+
middleJunction: "+",
|
|
503
|
+
bottomJunction: "+"
|
|
504
|
+
};
|
|
505
|
+
const widths = headers.map(
|
|
506
|
+
(header, index) => Math.max(
|
|
507
|
+
header.length,
|
|
508
|
+
...rows.map((row) => (row[index] ?? "").length)
|
|
509
|
+
)
|
|
510
|
+
);
|
|
511
|
+
const formatTableBorder = (left, join2, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join2)}${right}`;
|
|
512
|
+
const formatTableRow = (cells) => `${border.vertical}${cells.map((cell, index) => ` ${(cell ?? "").padEnd(widths[index])} `).join(border.vertical)}${border.vertical}`;
|
|
513
|
+
return [
|
|
514
|
+
formatTableBorder(border.topLeft, border.topJunction, border.topRight),
|
|
515
|
+
formatTableRow(headers),
|
|
516
|
+
formatTableBorder(
|
|
517
|
+
border.separatorLeft,
|
|
518
|
+
border.middleJunction,
|
|
519
|
+
border.separatorRight
|
|
520
|
+
),
|
|
521
|
+
...rows.map(formatTableRow),
|
|
522
|
+
formatTableBorder(
|
|
523
|
+
border.bottomLeft,
|
|
524
|
+
border.bottomJunction,
|
|
525
|
+
border.bottomRight
|
|
526
|
+
)
|
|
527
|
+
].join("\n");
|
|
528
|
+
}
|
|
529
|
+
function writeJson(value) {
|
|
530
|
+
writeStdout(JSON.stringify(value, null, 2));
|
|
531
|
+
}
|
|
532
|
+
|
|
386
533
|
// src/lib/links.ts
|
|
387
534
|
var LIST_KEYS = ["urls", "items", "results"];
|
|
388
|
-
function
|
|
535
|
+
function asObject(value) {
|
|
389
536
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
390
537
|
}
|
|
391
|
-
function
|
|
538
|
+
function asString(value) {
|
|
392
539
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
393
540
|
}
|
|
394
|
-
function
|
|
541
|
+
function asNumber(value) {
|
|
395
542
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
396
543
|
}
|
|
397
|
-
function
|
|
544
|
+
function pickText(link, keys) {
|
|
398
545
|
for (const key of keys) {
|
|
399
|
-
const value =
|
|
546
|
+
const value = asString(link[key]);
|
|
400
547
|
if (value) {
|
|
401
548
|
return value;
|
|
402
549
|
}
|
|
@@ -406,32 +553,32 @@ function firstString(link, keys) {
|
|
|
406
553
|
function truncate(value, maxLength) {
|
|
407
554
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
408
555
|
}
|
|
409
|
-
function
|
|
410
|
-
const record =
|
|
556
|
+
function getListMeta(data) {
|
|
557
|
+
const record = asObject(data);
|
|
411
558
|
if (!record) {
|
|
412
559
|
return null;
|
|
413
560
|
}
|
|
414
|
-
const meta =
|
|
561
|
+
const meta = asObject(record.meta);
|
|
415
562
|
if (meta) {
|
|
416
563
|
return {
|
|
417
|
-
page:
|
|
418
|
-
limit:
|
|
419
|
-
totalItems:
|
|
420
|
-
totalPages:
|
|
564
|
+
page: asNumber(meta.page),
|
|
565
|
+
limit: asNumber(meta.limit),
|
|
566
|
+
totalItems: asNumber(meta.totalItems),
|
|
567
|
+
totalPages: asNumber(meta.totalPages)
|
|
421
568
|
};
|
|
422
569
|
}
|
|
423
570
|
return {
|
|
424
|
-
page:
|
|
425
|
-
limit:
|
|
426
|
-
totalItems:
|
|
427
|
-
totalPages:
|
|
571
|
+
page: asNumber(record.page),
|
|
572
|
+
limit: asNumber(record.limit),
|
|
573
|
+
totalItems: asNumber(record.total),
|
|
574
|
+
totalPages: asNumber(record.totalPages)
|
|
428
575
|
};
|
|
429
576
|
}
|
|
430
577
|
function extractLinks(data) {
|
|
431
578
|
if (Array.isArray(data)) {
|
|
432
579
|
return data;
|
|
433
580
|
}
|
|
434
|
-
const record =
|
|
581
|
+
const record = asObject(data);
|
|
435
582
|
if (record) {
|
|
436
583
|
for (const key of LIST_KEYS) {
|
|
437
584
|
const value = record[key];
|
|
@@ -443,16 +590,16 @@ function extractLinks(data) {
|
|
|
443
590
|
return [];
|
|
444
591
|
}
|
|
445
592
|
function getLinkId(link) {
|
|
446
|
-
return
|
|
593
|
+
return pickText(link, ["id", "_id", "urlId"]) || (link.id !== void 0 ? String(link.id) : void 0);
|
|
447
594
|
}
|
|
448
595
|
function getLinkAlias(link) {
|
|
449
|
-
return
|
|
596
|
+
return pickText(link, ["alias", "shortCode", "slug", "code"]);
|
|
450
597
|
}
|
|
451
598
|
function getLinkShortUrl(link) {
|
|
452
|
-
return
|
|
599
|
+
return pickText(link, ["shortUrl", "shortLink", "shortURL", "url"]);
|
|
453
600
|
}
|
|
454
601
|
function getLinkDestination(link) {
|
|
455
|
-
return
|
|
602
|
+
return pickText(link, [
|
|
456
603
|
"destinationUrl",
|
|
457
604
|
"originalUrl",
|
|
458
605
|
"targetUrl",
|
|
@@ -468,14 +615,14 @@ function formatLinkDetails(link) {
|
|
|
468
615
|
["Alias", getLinkAlias(link)],
|
|
469
616
|
["Short URL", getLinkShortUrl(link)],
|
|
470
617
|
["Destination", getLinkDestination(link)],
|
|
471
|
-
["Title",
|
|
472
|
-
["Status",
|
|
618
|
+
["Title", asString(link.title)],
|
|
619
|
+
["Status", asString(link.status)],
|
|
473
620
|
[
|
|
474
621
|
"Clicks",
|
|
475
|
-
|
|
622
|
+
asNumber(link.clicks) === void 0 ? void 0 : String(link.clicks)
|
|
476
623
|
],
|
|
477
|
-
["Created",
|
|
478
|
-
["Updated",
|
|
624
|
+
["Created", asString(link.createdAt)],
|
|
625
|
+
["Updated", asString(link.updatedAt)]
|
|
479
626
|
].filter((entry) => Boolean(entry[1]));
|
|
480
627
|
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
481
628
|
}
|
|
@@ -483,26 +630,18 @@ function formatLinksTable(links) {
|
|
|
483
630
|
if (links.length === 0) {
|
|
484
631
|
return "No links found.";
|
|
485
632
|
}
|
|
486
|
-
const headers = ["ID", "
|
|
633
|
+
const headers = ["ID", "Alias", "Short URL", "Destination", "Status"];
|
|
487
634
|
const rows = links.map((link) => [
|
|
488
635
|
truncate(getLinkId(link) || "-", 18),
|
|
489
|
-
truncate(getLinkAlias(link) || "-",
|
|
636
|
+
truncate(getLinkAlias(link) || "-", 12),
|
|
490
637
|
truncate(getLinkShortUrl(link) || "-", 36),
|
|
491
638
|
truncate(getLinkDestination(link) || "-", 52),
|
|
492
|
-
truncate(
|
|
639
|
+
truncate(asString(link.status) || "-", 12)
|
|
493
640
|
]);
|
|
494
|
-
|
|
495
|
-
(header, index) => Math.max(header.length, ...rows.map((row) => row[index].length))
|
|
496
|
-
);
|
|
497
|
-
const renderRow = (row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
|
498
|
-
return [
|
|
499
|
-
renderRow(headers),
|
|
500
|
-
renderRow(widths.map((width) => "-".repeat(width))),
|
|
501
|
-
...rows.map(renderRow)
|
|
502
|
-
].join("\n");
|
|
641
|
+
return formatTable(headers, rows);
|
|
503
642
|
}
|
|
504
643
|
function formatListSummary(data, count) {
|
|
505
|
-
const meta =
|
|
644
|
+
const meta = getListMeta(data);
|
|
506
645
|
if (!meta) {
|
|
507
646
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
508
647
|
}
|
|
@@ -515,227 +654,32 @@ function formatListSummary(data, count) {
|
|
|
515
654
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
516
655
|
}
|
|
517
656
|
|
|
518
|
-
// src/lib/output.ts
|
|
519
|
-
function writeStdout(message = "") {
|
|
520
|
-
process.stdout.write(`${message}
|
|
521
|
-
`);
|
|
522
|
-
}
|
|
523
|
-
function writeStderr(message = "") {
|
|
524
|
-
process.stderr.write(`${message}
|
|
525
|
-
`);
|
|
526
|
-
}
|
|
527
|
-
function writeNoticeBox(title, lines, target = "stderr") {
|
|
528
|
-
const contentLines = lines.length > 0 ? lines : [""];
|
|
529
|
-
const width = Math.max(
|
|
530
|
-
title.length,
|
|
531
|
-
...contentLines.map((line) => line.length)
|
|
532
|
-
);
|
|
533
|
-
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
534
|
-
const useTuiBox = stream.isTTY;
|
|
535
|
-
const border = useTuiBox ? {
|
|
536
|
-
topLeft: "\u250C",
|
|
537
|
-
topRight: "\u2510",
|
|
538
|
-
bottomLeft: "\u2514",
|
|
539
|
-
bottomRight: "\u2518",
|
|
540
|
-
horizontal: "\u2500",
|
|
541
|
-
vertical: "\u2502",
|
|
542
|
-
separatorLeft: "\u251C",
|
|
543
|
-
separatorRight: "\u2524"
|
|
544
|
-
} : {
|
|
545
|
-
topLeft: "+",
|
|
546
|
-
topRight: "+",
|
|
547
|
-
bottomLeft: "+",
|
|
548
|
-
bottomRight: "+",
|
|
549
|
-
horizontal: "-",
|
|
550
|
-
vertical: "|",
|
|
551
|
-
separatorLeft: "+",
|
|
552
|
-
separatorRight: "+"
|
|
553
|
-
};
|
|
554
|
-
const topBorder = `${border.topLeft}${border.horizontal.repeat(width + 2)}${border.topRight}`;
|
|
555
|
-
const separator = `${border.separatorLeft}${border.horizontal.repeat(width + 2)}${border.separatorRight}`;
|
|
556
|
-
const bottomBorder = `${border.bottomLeft}${border.horizontal.repeat(width + 2)}${border.bottomRight}`;
|
|
557
|
-
const writeLine = target === "stdout" ? writeStdout : writeStderr;
|
|
558
|
-
writeLine(topBorder);
|
|
559
|
-
writeLine(`${border.vertical} ${title.padEnd(width)} ${border.vertical}`);
|
|
560
|
-
writeLine(separator);
|
|
561
|
-
for (const line of contentLines) {
|
|
562
|
-
writeLine(
|
|
563
|
-
`${border.vertical} ${line.padEnd(width)} ${border.vertical}`
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
writeLine(bottomBorder);
|
|
567
|
-
}
|
|
568
|
-
function writeJson(value) {
|
|
569
|
-
writeStdout(JSON.stringify(value, null, 2));
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// src/commands/create.ts
|
|
573
|
-
function normalizeExpiresAt(value) {
|
|
574
|
-
if (!value) {
|
|
575
|
-
return void 0;
|
|
576
|
-
}
|
|
577
|
-
if (Number.isNaN(Date.parse(value))) {
|
|
578
|
-
throw new CliError(`Invalid expiration timestamp: ${value}`);
|
|
579
|
-
}
|
|
580
|
-
return value;
|
|
581
|
-
}
|
|
582
|
-
async function createCommand(destinationUrl, options) {
|
|
583
|
-
const config = await resolveStoredConfig(process.env);
|
|
584
|
-
const response = await new PeakUrlApiClient(config).createUrl({
|
|
585
|
-
destinationUrl: normalizeDestinationUrl(destinationUrl),
|
|
586
|
-
...options.alias ? { alias: options.alias } : {},
|
|
587
|
-
...options.title ? { title: options.title } : {},
|
|
588
|
-
...options.password ? { password: options.password } : {},
|
|
589
|
-
...options.status ? { status: options.status } : {},
|
|
590
|
-
...options.expiresAt ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
|
|
591
|
-
...options.utmSource ? { utmSource: options.utmSource } : {},
|
|
592
|
-
...options.utmMedium ? { utmMedium: options.utmMedium } : {},
|
|
593
|
-
...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
|
|
594
|
-
...options.utmTerm ? { utmTerm: options.utmTerm } : {},
|
|
595
|
-
...options.utmContent ? { utmContent: options.utmContent } : {}
|
|
596
|
-
});
|
|
597
|
-
if (options.json) {
|
|
598
|
-
writeJson(response);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (options.quiet) {
|
|
602
|
-
writeStdout(getQuietLinkValue(response.data));
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
writeStdout(response.message);
|
|
606
|
-
writeStdout(formatLinkDetails(response.data));
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// src/commands/delete.ts
|
|
610
|
-
async function deleteCommand(idOrAlias, options) {
|
|
611
|
-
const config = await resolveStoredConfig(process.env);
|
|
612
|
-
const client = new PeakUrlApiClient(config);
|
|
613
|
-
const lookupResponse = await client.getUrl(idOrAlias);
|
|
614
|
-
const resolvedId = getLinkId(lookupResponse.data);
|
|
615
|
-
if (!resolvedId) {
|
|
616
|
-
throw new CliError(
|
|
617
|
-
"PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
const response = await client.deleteUrl(resolvedId);
|
|
621
|
-
if (options.json) {
|
|
622
|
-
writeJson(response);
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
if (options.quiet) {
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
writeStdout(response.message);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// src/commands/get.ts
|
|
632
|
-
async function getCommand(idOrAlias, options) {
|
|
633
|
-
const config = await resolveStoredConfig(process.env);
|
|
634
|
-
const response = await new PeakUrlApiClient(config).getUrl(idOrAlias);
|
|
635
|
-
if (options.json) {
|
|
636
|
-
writeJson(response);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
if (options.quiet) {
|
|
640
|
-
writeStdout(getQuietLinkValue(response.data));
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
writeStdout(response.message);
|
|
644
|
-
writeStdout(formatLinkDetails(response.data));
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// src/commands/list.ts
|
|
648
|
-
async function listCommand(options) {
|
|
649
|
-
const config = await resolveStoredConfig(process.env);
|
|
650
|
-
const response = await new PeakUrlApiClient(config).listUrls({
|
|
651
|
-
page: options.page,
|
|
652
|
-
limit: options.limit,
|
|
653
|
-
search: options.search,
|
|
654
|
-
sortBy: options.sortBy,
|
|
655
|
-
sortOrder: options.sortOrder
|
|
656
|
-
});
|
|
657
|
-
const links = extractLinks(response.data);
|
|
658
|
-
if (options.json) {
|
|
659
|
-
writeJson(response);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (options.quiet) {
|
|
663
|
-
for (const link of links) {
|
|
664
|
-
const value = getQuietLinkValue(link);
|
|
665
|
-
if (value) {
|
|
666
|
-
writeStdout(value);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
writeStdout(response.message);
|
|
672
|
-
writeStdout(formatLinksTable(links));
|
|
673
|
-
writeStdout(formatListSummary(response.data, links.length));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// src/lib/users.ts
|
|
677
|
-
function readString2(value) {
|
|
678
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
679
|
-
}
|
|
680
|
-
function getUserLabel(user) {
|
|
681
|
-
const fullName = [readString2(user.firstName), readString2(user.lastName)].filter(Boolean).join(" ");
|
|
682
|
-
return fullName || readString2(user.username) || readString2(user.email) || String(user.id ?? "unknown");
|
|
683
|
-
}
|
|
684
|
-
function getQuietUserValue(user) {
|
|
685
|
-
return readString2(user.username) || readString2(user.email) || String(user.id ?? "");
|
|
686
|
-
}
|
|
687
|
-
function formatUserDetails(user) {
|
|
688
|
-
const lines = [
|
|
689
|
-
["Name", getUserLabel(user)],
|
|
690
|
-
["Username", readString2(user.username)],
|
|
691
|
-
["Email", readString2(user.email)],
|
|
692
|
-
["Role", readString2(user.role)],
|
|
693
|
-
["ID", user.id === void 0 ? void 0 : String(user.id)]
|
|
694
|
-
].filter((entry) => Boolean(entry[1]));
|
|
695
|
-
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// src/commands/login.ts
|
|
699
|
-
async function loginCommand(options) {
|
|
700
|
-
const credentials = resolveLoginConfig(options, process.env);
|
|
701
|
-
const client = new PeakUrlApiClient(credentials);
|
|
702
|
-
const response = await client.whoami();
|
|
703
|
-
await new ConfigStore().save(credentials);
|
|
704
|
-
const payload = {
|
|
705
|
-
success: true,
|
|
706
|
-
message: `Saved credentials for ${credentials.baseUrl}.`,
|
|
707
|
-
data: {
|
|
708
|
-
baseUrl: credentials.baseUrl,
|
|
709
|
-
user: response.data
|
|
710
|
-
},
|
|
711
|
-
timestamp: response.timestamp
|
|
712
|
-
};
|
|
713
|
-
if (options.json) {
|
|
714
|
-
writeJson(payload);
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
if (options.quiet) {
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
writeStdout(`Saved credentials for ${credentials.baseUrl}`);
|
|
721
|
-
writeStdout(`Authenticated as ${getUserLabel(response.data)}`);
|
|
722
|
-
writeStdout(formatUserDetails(response.data));
|
|
723
|
-
}
|
|
724
|
-
|
|
725
657
|
// src/lib/update.ts
|
|
726
658
|
var PACKAGE_NAME = "peakurl";
|
|
727
659
|
var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
728
660
|
var CACHE_TTL_MS = 1e3 * 60 * 60 * 12;
|
|
729
661
|
var NOTICE_TTL_MS = 1e3 * 60 * 60 * 24;
|
|
730
|
-
function
|
|
662
|
+
function parseTime(value) {
|
|
731
663
|
if (!value) {
|
|
732
664
|
return null;
|
|
733
665
|
}
|
|
734
666
|
const parsed = Date.parse(value);
|
|
735
667
|
return Number.isNaN(parsed) ? null : parsed;
|
|
736
668
|
}
|
|
737
|
-
function
|
|
738
|
-
|
|
669
|
+
function getRegistryBaseUrl(env) {
|
|
670
|
+
const candidate = env.PEAKURL_NPM_REGISTRY_URL?.trim();
|
|
671
|
+
if (!candidate) {
|
|
672
|
+
return DEFAULT_REGISTRY_URL;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
const parsed = new URL(candidate);
|
|
676
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password) {
|
|
677
|
+
return DEFAULT_REGISTRY_URL;
|
|
678
|
+
}
|
|
679
|
+
return candidate;
|
|
680
|
+
} catch {
|
|
681
|
+
return DEFAULT_REGISTRY_URL;
|
|
682
|
+
}
|
|
739
683
|
}
|
|
740
684
|
function normalizeRegistryUrl(registryUrl) {
|
|
741
685
|
return registryUrl.replace(/\/+$/, "");
|
|
@@ -811,11 +755,11 @@ function compareSemver(left, right) {
|
|
|
811
755
|
}
|
|
812
756
|
return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
|
|
813
757
|
}
|
|
814
|
-
function
|
|
758
|
+
function getUpdateInstallCommand() {
|
|
815
759
|
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
816
760
|
}
|
|
817
761
|
async function fetchLatestVersion(env) {
|
|
818
|
-
const registryUrl = normalizeRegistryUrl(
|
|
762
|
+
const registryUrl = normalizeRegistryUrl(getRegistryBaseUrl(env));
|
|
819
763
|
const url = `${registryUrl}/${PACKAGE_NAME}/latest`;
|
|
820
764
|
const controller = new AbortController();
|
|
821
765
|
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
@@ -837,7 +781,7 @@ async function fetchLatestVersion(env) {
|
|
|
837
781
|
clearTimeout(timeout);
|
|
838
782
|
}
|
|
839
783
|
}
|
|
840
|
-
async function
|
|
784
|
+
async function getUpdateState(store) {
|
|
841
785
|
const state = await store.load();
|
|
842
786
|
return state.update ?? {};
|
|
843
787
|
}
|
|
@@ -848,10 +792,10 @@ async function saveUpdateState(store, update) {
|
|
|
848
792
|
update
|
|
849
793
|
});
|
|
850
794
|
}
|
|
851
|
-
async function
|
|
795
|
+
async function loadLatestVersion(env, options) {
|
|
852
796
|
const store = options?.store ?? new StateStore();
|
|
853
|
-
const updateState = await
|
|
854
|
-
const lastCheckedAt =
|
|
797
|
+
const updateState = await getUpdateState(store);
|
|
798
|
+
const lastCheckedAt = parseTime(updateState.lastCheckedAt);
|
|
855
799
|
const now = Date.now();
|
|
856
800
|
if (!options?.forceRefresh && updateState.latestVersion && lastCheckedAt !== null && now - lastCheckedAt < CACHE_TTL_MS) {
|
|
857
801
|
return updateState.latestVersion;
|
|
@@ -868,7 +812,7 @@ async function resolveLatestVersion(env, options) {
|
|
|
868
812
|
return latestVersion;
|
|
869
813
|
}
|
|
870
814
|
async function getUpdateStatus(currentVersion, env, options) {
|
|
871
|
-
const resolvedLatestVersion = await
|
|
815
|
+
const resolvedLatestVersion = await loadLatestVersion(env, {
|
|
872
816
|
forceRefresh: options?.forceRefresh,
|
|
873
817
|
store: options?.store
|
|
874
818
|
});
|
|
@@ -882,16 +826,16 @@ async function getUpdateStatus(currentVersion, env, options) {
|
|
|
882
826
|
currentVersion,
|
|
883
827
|
latestVersion,
|
|
884
828
|
isOutdated: compareSemver(currentVersion, latestVersion) < 0,
|
|
885
|
-
installCommand:
|
|
829
|
+
installCommand: getUpdateInstallCommand()
|
|
886
830
|
};
|
|
887
831
|
}
|
|
888
|
-
function
|
|
832
|
+
function showUpdateNotice(status) {
|
|
889
833
|
writeNoticeBox("Update Available", [
|
|
890
834
|
`${PACKAGE_NAME} ${status.currentVersion} -> ${status.latestVersion}`,
|
|
891
835
|
`Run: ${status.installCommand}`
|
|
892
836
|
]);
|
|
893
837
|
}
|
|
894
|
-
async function
|
|
838
|
+
async function checkUpdates(options) {
|
|
895
839
|
if (options.env.PEAKURL_DISABLE_UPDATE_CHECK === "1" || options.commandName === "update" || options.options?.json || options.options?.quiet) {
|
|
896
840
|
return;
|
|
897
841
|
}
|
|
@@ -899,14 +843,14 @@ async function maybeShowUpdateNotice(options) {
|
|
|
899
843
|
return;
|
|
900
844
|
}
|
|
901
845
|
const store = new StateStore();
|
|
902
|
-
const updateState = await
|
|
846
|
+
const updateState = await getUpdateState(store);
|
|
903
847
|
const status = await getUpdateStatus(options.currentVersion, options.env, {
|
|
904
848
|
store
|
|
905
849
|
});
|
|
906
850
|
if (!status.isOutdated) {
|
|
907
851
|
return;
|
|
908
852
|
}
|
|
909
|
-
const lastNotifiedAt =
|
|
853
|
+
const lastNotifiedAt = parseTime(updateState.lastNotifiedAt);
|
|
910
854
|
const alreadyNotifiedForVersion = updateState.lastNotifiedVersion === status.latestVersion;
|
|
911
855
|
if (alreadyNotifiedForVersion && lastNotifiedAt !== null && Date.now() - lastNotifiedAt < NOTICE_TTL_MS) {
|
|
912
856
|
return;
|
|
@@ -916,7 +860,198 @@ async function maybeShowUpdateNotice(options) {
|
|
|
916
860
|
lastNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
917
861
|
lastNotifiedVersion: status.latestVersion
|
|
918
862
|
});
|
|
919
|
-
|
|
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
|
+
}
|
|
920
1055
|
}
|
|
921
1056
|
|
|
922
1057
|
// src/commands/update.ts
|
|
@@ -924,7 +1059,7 @@ async function updateCommand(options, currentVersion) {
|
|
|
924
1059
|
const status = await getUpdateStatus(currentVersion, process.env, {
|
|
925
1060
|
forceRefresh: true
|
|
926
1061
|
});
|
|
927
|
-
const
|
|
1062
|
+
const responseBody = {
|
|
928
1063
|
success: true,
|
|
929
1064
|
message: status.isOutdated ? `A newer PeakURL CLI version is available (${status.latestVersion}).` : `PeakURL CLI ${status.currentVersion} is up to date.`,
|
|
930
1065
|
data: {
|
|
@@ -937,7 +1072,7 @@ async function updateCommand(options, currentVersion) {
|
|
|
937
1072
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
938
1073
|
};
|
|
939
1074
|
if (options.json) {
|
|
940
|
-
writeJson(
|
|
1075
|
+
writeJson(responseBody);
|
|
941
1076
|
return;
|
|
942
1077
|
}
|
|
943
1078
|
if (!status.isOutdated) {
|
|
@@ -962,22 +1097,22 @@ async function updateCommand(options, currentVersion) {
|
|
|
962
1097
|
|
|
963
1098
|
// src/commands/whoami.ts
|
|
964
1099
|
async function whoamiCommand(options) {
|
|
965
|
-
const config = await
|
|
966
|
-
const response = await new
|
|
1100
|
+
const config = await getAuthConfig(process.env);
|
|
1101
|
+
const response = await new ApiClient(config).whoami();
|
|
967
1102
|
if (options.json) {
|
|
968
1103
|
writeJson(response);
|
|
969
1104
|
return;
|
|
970
1105
|
}
|
|
971
1106
|
if (options.quiet) {
|
|
972
|
-
writeStdout(
|
|
1107
|
+
writeStdout(userValue(response.data));
|
|
973
1108
|
return;
|
|
974
1109
|
}
|
|
975
1110
|
writeStdout(response.message);
|
|
976
|
-
writeStdout(
|
|
1111
|
+
writeStdout(userTable(response.data));
|
|
977
1112
|
}
|
|
978
1113
|
|
|
979
1114
|
// src/index.ts
|
|
980
|
-
function
|
|
1115
|
+
function parseNumber(label) {
|
|
981
1116
|
return (value) => {
|
|
982
1117
|
const parsed = Number.parseInt(value, 10);
|
|
983
1118
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -988,7 +1123,7 @@ function parsePositiveInteger(label) {
|
|
|
988
1123
|
return parsed;
|
|
989
1124
|
};
|
|
990
1125
|
}
|
|
991
|
-
async function
|
|
1126
|
+
async function getCliVersion() {
|
|
992
1127
|
const packageJson = new URL("../package.json", import.meta.url);
|
|
993
1128
|
const content = await readFile2(packageJson, "utf8");
|
|
994
1129
|
const parsed = JSON.parse(content);
|
|
@@ -996,13 +1131,14 @@ async function readVersion() {
|
|
|
996
1131
|
}
|
|
997
1132
|
async function main() {
|
|
998
1133
|
const program = new Command();
|
|
999
|
-
const version = await
|
|
1134
|
+
const version = await getCliVersion();
|
|
1000
1135
|
program.name("peakurl").description("PeakURL command-line interface").version(version).showHelpAfterError().showSuggestionAfterError().addHelpText(
|
|
1001
1136
|
"after",
|
|
1002
1137
|
`
|
|
1003
1138
|
Examples:
|
|
1004
1139
|
peakurl login --base-url https://peakurl.org --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
1005
1140
|
peakurl whoami --json
|
|
1141
|
+
peakurl logout
|
|
1006
1142
|
peakurl create https://example.com --alias example
|
|
1007
1143
|
peakurl list --limit 10
|
|
1008
1144
|
peakurl update --check
|
|
@@ -1011,7 +1147,7 @@ Examples:
|
|
|
1011
1147
|
).exitOverride();
|
|
1012
1148
|
program.hook("preAction", async (_command, actionCommand) => {
|
|
1013
1149
|
const options = actionCommand.optsWithGlobals();
|
|
1014
|
-
await
|
|
1150
|
+
await checkUpdates({
|
|
1015
1151
|
currentVersion: version,
|
|
1016
1152
|
commandName: actionCommand.name(),
|
|
1017
1153
|
options,
|
|
@@ -1025,11 +1161,12 @@ Examples:
|
|
|
1025
1161
|
"PeakURL base URL, for example https://peakurl.org"
|
|
1026
1162
|
).option("--api-key <token>", "PeakURL API key to store").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(loginCommand);
|
|
1027
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);
|
|
1028
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(
|
|
1029
1166
|
"--status <status>",
|
|
1030
1167
|
"Link status, for example active or paused"
|
|
1031
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);
|
|
1032
|
-
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);
|
|
1033
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);
|
|
1034
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);
|
|
1035
1172
|
program.command("update").description(
|
|
@@ -1044,8 +1181,13 @@ Examples:
|
|
|
1044
1181
|
if (error instanceof CommanderError) {
|
|
1045
1182
|
process.exit(error.exitCode);
|
|
1046
1183
|
}
|
|
1047
|
-
const cliError =
|
|
1048
|
-
|
|
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
|
+
}
|
|
1049
1191
|
process.exit(cliError.exitCode);
|
|
1050
1192
|
}
|
|
1051
1193
|
}
|