peakurl 0.2.0 → 0.3.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 +100 -13
- package/bin/peakurl.js +1202 -112
- package/package.json +1 -1
package/bin/peakurl.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFile as
|
|
4
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5
5
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
6
6
|
|
|
7
|
+
// src/commands/links.ts
|
|
8
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
9
|
+
import { dirname as dirname2, resolve } from "path";
|
|
10
|
+
|
|
7
11
|
// src/lib/errors.ts
|
|
8
12
|
var CliError = class extends Error {
|
|
9
13
|
exitCode;
|
|
@@ -34,28 +38,30 @@ function validateHttpUrl(parsed, label) {
|
|
|
34
38
|
throw new CliError(`${label} must not include embedded credentials.`);
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
function
|
|
41
|
+
function getApiBaseUrl(value) {
|
|
38
42
|
const input = value.trim();
|
|
39
43
|
if (!input) {
|
|
40
|
-
throw new CliError("A PeakURL base URL is required.");
|
|
44
|
+
throw new CliError("A PeakURL API base URL is required.");
|
|
41
45
|
}
|
|
42
46
|
let parsed;
|
|
43
47
|
try {
|
|
44
48
|
parsed = new URL(input);
|
|
45
49
|
} catch {
|
|
46
|
-
throw new CliError(`Invalid base URL: ${value}`);
|
|
50
|
+
throw new CliError(`Invalid API base URL: ${value}`);
|
|
47
51
|
}
|
|
48
|
-
validateHttpUrl(parsed, "PeakURL base URL");
|
|
52
|
+
validateHttpUrl(parsed, "PeakURL API base URL");
|
|
49
53
|
parsed.hash = "";
|
|
50
54
|
parsed.search = "";
|
|
51
55
|
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
if (!/\/api\/v1$/i.test(pathname)) {
|
|
57
|
+
throw new CliError("PeakURL API base URL must end with /api/v1.");
|
|
58
|
+
}
|
|
59
|
+
return `${parsed.origin}${pathname}`;
|
|
54
60
|
}
|
|
55
|
-
function buildApiUrl(
|
|
56
|
-
const cleanBaseUrl =
|
|
61
|
+
function buildApiUrl(apiBaseUrl, path, query) {
|
|
62
|
+
const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
|
|
57
63
|
const cleanPath = path.replace(/^\/+/, "");
|
|
58
|
-
const url = new URL(
|
|
64
|
+
const url = new URL(cleanPath, `${cleanBaseUrl}/`);
|
|
59
65
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
60
66
|
if (value === void 0 || value === "") {
|
|
61
67
|
continue;
|
|
@@ -80,6 +86,22 @@ function normalizeDestinationUrl(value) {
|
|
|
80
86
|
throw new CliError(`Invalid destination URL: ${value}`);
|
|
81
87
|
}
|
|
82
88
|
}
|
|
89
|
+
function normalizeWebhookUrl(value) {
|
|
90
|
+
const input = value.trim();
|
|
91
|
+
if (!input) {
|
|
92
|
+
throw new CliError("A webhook URL is required.");
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(input);
|
|
96
|
+
validateHttpUrl(parsed, "Webhook URL");
|
|
97
|
+
return parsed.toString();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof CliError) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
throw new CliError(`Invalid webhook URL: ${value}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
83
105
|
|
|
84
106
|
// src/api/client.ts
|
|
85
107
|
function isApiResponse(value) {
|
|
@@ -87,17 +109,17 @@ function isApiResponse(value) {
|
|
|
87
109
|
value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
|
|
88
110
|
);
|
|
89
111
|
}
|
|
90
|
-
function networkError(
|
|
112
|
+
function networkError(apiBaseUrl, error) {
|
|
91
113
|
if (error instanceof Error && error.message) {
|
|
92
|
-
return `Could not reach PeakURL at ${
|
|
114
|
+
return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
|
|
93
115
|
}
|
|
94
|
-
return `Could not reach PeakURL at ${
|
|
116
|
+
return `Could not reach PeakURL at ${apiBaseUrl}.`;
|
|
95
117
|
}
|
|
96
118
|
var ApiClient = class {
|
|
97
119
|
/**
|
|
98
120
|
* Creates a client bound to one resolved credential set.
|
|
99
121
|
*
|
|
100
|
-
* @param config
|
|
122
|
+
* @param config Explicit API base URL plus bearer API key.
|
|
101
123
|
*/
|
|
102
124
|
constructor(config) {
|
|
103
125
|
this.config = config;
|
|
@@ -114,6 +136,14 @@ var ApiClient = class {
|
|
|
114
136
|
whoami() {
|
|
115
137
|
return this.request("GET", "users/me");
|
|
116
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Loads the current system status snapshot for the authenticated site.
|
|
141
|
+
*
|
|
142
|
+
* @returns API response envelope containing system status sections.
|
|
143
|
+
*/
|
|
144
|
+
getStatus() {
|
|
145
|
+
return this.request("GET", "system/status");
|
|
146
|
+
}
|
|
117
147
|
/**
|
|
118
148
|
* Creates a short URL.
|
|
119
149
|
*
|
|
@@ -135,6 +165,29 @@ var ApiClient = class {
|
|
|
135
165
|
listUrls(query) {
|
|
136
166
|
return this.request("GET", "urls", void 0, query);
|
|
137
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Exports the full accessible link dataset for the authenticated user.
|
|
170
|
+
*
|
|
171
|
+
* @param query Optional search and sort values.
|
|
172
|
+
* @returns API response envelope containing the full export payload.
|
|
173
|
+
*/
|
|
174
|
+
exportUrls(query) {
|
|
175
|
+
return this.request(
|
|
176
|
+
"GET",
|
|
177
|
+
"urls/export",
|
|
178
|
+
void 0,
|
|
179
|
+
query
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Imports multiple short links in one bulk request.
|
|
184
|
+
*
|
|
185
|
+
* @param payload Request body accepted by `POST /api/v1/urls/bulk`.
|
|
186
|
+
* @returns API response envelope containing created rows plus row errors.
|
|
187
|
+
*/
|
|
188
|
+
importUrls(payload) {
|
|
189
|
+
return this.request("POST", "urls/bulk", payload);
|
|
190
|
+
}
|
|
138
191
|
/**
|
|
139
192
|
* Loads a single short URL by identifier or alias.
|
|
140
193
|
*
|
|
@@ -164,6 +217,35 @@ var ApiClient = class {
|
|
|
164
217
|
`urls/${encodeURIComponent(id)}`
|
|
165
218
|
);
|
|
166
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Lists outbound webhooks for the authenticated user.
|
|
222
|
+
*
|
|
223
|
+
* @returns API response envelope containing webhook rows.
|
|
224
|
+
*/
|
|
225
|
+
listWebhooks() {
|
|
226
|
+
return this.request("GET", "webhooks");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Creates one outbound webhook subscription.
|
|
230
|
+
*
|
|
231
|
+
* @param payload Request body accepted by `POST /api/v1/webhooks`.
|
|
232
|
+
* @returns API response envelope containing the created webhook.
|
|
233
|
+
*/
|
|
234
|
+
createWebhook(payload) {
|
|
235
|
+
return this.request("POST", "webhooks", payload);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Deletes one webhook by its stable row ID.
|
|
239
|
+
*
|
|
240
|
+
* @param id Webhook identifier returned by the list/create endpoints.
|
|
241
|
+
* @returns API response envelope containing the deletion result.
|
|
242
|
+
*/
|
|
243
|
+
deleteWebhook(id) {
|
|
244
|
+
return this.request(
|
|
245
|
+
"DELETE",
|
|
246
|
+
`webhooks/${encodeURIComponent(id)}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
167
249
|
/**
|
|
168
250
|
* Performs one authenticated API request and normalizes the response.
|
|
169
251
|
*
|
|
@@ -175,7 +257,7 @@ var ApiClient = class {
|
|
|
175
257
|
* @throws {CliError} When the network request fails or the API returns an error.
|
|
176
258
|
*/
|
|
177
259
|
async request(method, path, body, query) {
|
|
178
|
-
const url = buildApiUrl(this.config.
|
|
260
|
+
const url = buildApiUrl(this.config.apiBaseUrl, path, query);
|
|
179
261
|
let response;
|
|
180
262
|
try {
|
|
181
263
|
response = await fetch(url, {
|
|
@@ -188,7 +270,7 @@ var ApiClient = class {
|
|
|
188
270
|
body: body ? JSON.stringify(body) : void 0
|
|
189
271
|
});
|
|
190
272
|
} catch (error) {
|
|
191
|
-
throw new CliError(networkError(this.config.
|
|
273
|
+
throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
|
|
192
274
|
cause: error instanceof Error ? error : void 0
|
|
193
275
|
});
|
|
194
276
|
}
|
|
@@ -273,11 +355,12 @@ var ConfigStore = class {
|
|
|
273
355
|
try {
|
|
274
356
|
const content = await readFile(this.filePath, "utf8");
|
|
275
357
|
const parsed = JSON.parse(content);
|
|
276
|
-
|
|
358
|
+
const apiBaseUrl = typeof parsed?.apiBaseUrl === "string" ? parsed.apiBaseUrl : typeof parsed?.baseUrl === "string" ? parsed.baseUrl : void 0;
|
|
359
|
+
if (typeof apiBaseUrl !== "string" || typeof parsed?.apiKey !== "string") {
|
|
277
360
|
throw new CliError(`Invalid config file: ${this.filePath}`);
|
|
278
361
|
}
|
|
279
362
|
return {
|
|
280
|
-
|
|
363
|
+
apiBaseUrl,
|
|
281
364
|
apiKey: parsed.apiKey
|
|
282
365
|
};
|
|
283
366
|
} catch (error) {
|
|
@@ -388,42 +471,158 @@ var StateStore = class {
|
|
|
388
471
|
|
|
389
472
|
// src/lib/auth.ts
|
|
390
473
|
var AUTH_REQUIRED_MESSAGE = "PeakURL credentials are not configured.";
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
[
|
|
474
|
+
var EXAMPLE_BASE_URL = "https://example.com/api/v1";
|
|
475
|
+
var EXAMPLE_API_KEY = "YOUR_API_KEY";
|
|
476
|
+
function authRows(commandName) {
|
|
477
|
+
const retryCommand = commandName ? `peakurl ${commandName}` : "peakurl whoami";
|
|
478
|
+
const rows2 = [
|
|
479
|
+
[
|
|
480
|
+
"Save credentials",
|
|
481
|
+
`peakurl login --base-url ${EXAMPLE_BASE_URL}
|
|
482
|
+
--api-key ${EXAMPLE_API_KEY}`,
|
|
483
|
+
"Regular use on this machine"
|
|
484
|
+
],
|
|
485
|
+
[
|
|
486
|
+
"Set environment variables",
|
|
487
|
+
`PEAKURL_BASE_URL=${EXAMPLE_BASE_URL}
|
|
488
|
+
PEAKURL_API_KEY=${EXAMPLE_API_KEY}`,
|
|
489
|
+
"CI, scripts, or one-off use"
|
|
490
|
+
],
|
|
491
|
+
["Then run", retryCommand, "After completing one of the steps above"]
|
|
397
492
|
];
|
|
493
|
+
return rows2;
|
|
398
494
|
}
|
|
399
495
|
function getLoginConfig(input, env) {
|
|
400
|
-
const
|
|
496
|
+
const apiBaseUrl = input.baseUrl?.trim() || env.PEAKURL_BASE_URL?.trim();
|
|
401
497
|
const apiKey = input.apiKey?.trim() || env.PEAKURL_API_KEY?.trim();
|
|
402
|
-
if (!
|
|
498
|
+
if (!apiBaseUrl || !apiKey) {
|
|
403
499
|
throw new CliError(
|
|
404
500
|
"Missing credentials. Provide --base-url and --api-key, or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
|
|
405
501
|
);
|
|
406
502
|
}
|
|
407
503
|
return {
|
|
408
|
-
|
|
504
|
+
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
409
505
|
apiKey
|
|
410
506
|
};
|
|
411
507
|
}
|
|
412
508
|
async function getAuthConfig(env, store = new ConfigStore()) {
|
|
413
509
|
const saved = await store.load();
|
|
414
|
-
const
|
|
510
|
+
const apiBaseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.apiBaseUrl;
|
|
415
511
|
const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
|
|
416
|
-
if (!
|
|
512
|
+
if (!apiBaseUrl || !apiKey) {
|
|
417
513
|
throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
|
|
418
514
|
kind: "auth_required"
|
|
419
515
|
});
|
|
420
516
|
}
|
|
421
517
|
return {
|
|
422
|
-
|
|
518
|
+
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
423
519
|
apiKey
|
|
424
520
|
};
|
|
425
521
|
}
|
|
426
522
|
|
|
523
|
+
// src/lib/exports.ts
|
|
524
|
+
var EXPORT_HEADERS = [
|
|
525
|
+
"url",
|
|
526
|
+
"alias",
|
|
527
|
+
"title",
|
|
528
|
+
"password",
|
|
529
|
+
"expires",
|
|
530
|
+
"short_url",
|
|
531
|
+
"clicks",
|
|
532
|
+
"unique_clicks",
|
|
533
|
+
"created_at"
|
|
534
|
+
];
|
|
535
|
+
function text(value) {
|
|
536
|
+
return typeof value === "string" ? value : "";
|
|
537
|
+
}
|
|
538
|
+
function csvValue(value) {
|
|
539
|
+
const content = value == null ? "" : String(value);
|
|
540
|
+
if (/[",\r\n]/.test(content)) {
|
|
541
|
+
return `"${content.replace(/"/g, '""')}"`;
|
|
542
|
+
}
|
|
543
|
+
return content;
|
|
544
|
+
}
|
|
545
|
+
function xmlValue(value) {
|
|
546
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
547
|
+
}
|
|
548
|
+
function aliasValue(link) {
|
|
549
|
+
return text(link.alias) || text(link.shortCode);
|
|
550
|
+
}
|
|
551
|
+
function parseFileFormat(value) {
|
|
552
|
+
const format = value.trim().toLowerCase();
|
|
553
|
+
if (format === "csv" || format === "json" || format === "xml") {
|
|
554
|
+
return format;
|
|
555
|
+
}
|
|
556
|
+
throw new CliError(
|
|
557
|
+
`Unsupported file format: ${value}. Use csv, json, or xml.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
function getFileFormatFromPath(filePath) {
|
|
561
|
+
const value = filePath.trim().toLowerCase();
|
|
562
|
+
if (value.endsWith(".csv")) {
|
|
563
|
+
return "csv";
|
|
564
|
+
}
|
|
565
|
+
if (value.endsWith(".json")) {
|
|
566
|
+
return "json";
|
|
567
|
+
}
|
|
568
|
+
if (value.endsWith(".xml")) {
|
|
569
|
+
return "xml";
|
|
570
|
+
}
|
|
571
|
+
return void 0;
|
|
572
|
+
}
|
|
573
|
+
function getExportFileName(format) {
|
|
574
|
+
return `peakurl-links.${format}`;
|
|
575
|
+
}
|
|
576
|
+
function buildExportRows(links) {
|
|
577
|
+
return links.map((link) => ({
|
|
578
|
+
url: text(link.destinationUrl),
|
|
579
|
+
alias: aliasValue(link),
|
|
580
|
+
title: text(link.title),
|
|
581
|
+
password: "",
|
|
582
|
+
expires: text(link.expiresAt),
|
|
583
|
+
short_url: text(link.shortUrl),
|
|
584
|
+
clicks: typeof link.clicks === "number" ? link.clicks : "",
|
|
585
|
+
unique_clicks: typeof link.uniqueClicks === "number" ? link.uniqueClicks : "",
|
|
586
|
+
created_at: text(link.createdAt)
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
function serializeLinkExport(links, format) {
|
|
590
|
+
const rows2 = buildExportRows(links);
|
|
591
|
+
if (format === "json") {
|
|
592
|
+
return JSON.stringify(rows2, null, 2);
|
|
593
|
+
}
|
|
594
|
+
if (format === "xml") {
|
|
595
|
+
const body = rows2.map(
|
|
596
|
+
(row2) => ` <url>
|
|
597
|
+
<destinationUrl>${xmlValue(row2.url)}</destinationUrl>
|
|
598
|
+
<alias>${xmlValue(row2.alias)}</alias>
|
|
599
|
+
<title>${xmlValue(row2.title)}</title>
|
|
600
|
+
<password>${xmlValue(row2.password)}</password>
|
|
601
|
+
<expiresAt>${xmlValue(row2.expires)}</expiresAt>
|
|
602
|
+
<shortUrl>${xmlValue(row2.short_url)}</shortUrl>
|
|
603
|
+
<clicks>${xmlValue(row2.clicks)}</clicks>
|
|
604
|
+
<uniqueClicks>${xmlValue(row2.unique_clicks)}</uniqueClicks>
|
|
605
|
+
<createdAt>${xmlValue(row2.created_at)}</createdAt>
|
|
606
|
+
</url>`
|
|
607
|
+
).join("\n");
|
|
608
|
+
return `<urls>
|
|
609
|
+
${body}
|
|
610
|
+
</urls>
|
|
611
|
+
`;
|
|
612
|
+
}
|
|
613
|
+
const lines = [
|
|
614
|
+
EXPORT_HEADERS.join(","),
|
|
615
|
+
...rows2.map(
|
|
616
|
+
(row2) => EXPORT_HEADERS.map((key) => csvValue(row2[key])).join(",")
|
|
617
|
+
)
|
|
618
|
+
];
|
|
619
|
+
return `${lines.join("\n")}
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/lib/imports.ts
|
|
624
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
625
|
+
|
|
427
626
|
// src/lib/output.ts
|
|
428
627
|
function writeStdout(message = "") {
|
|
429
628
|
process.stdout.write(`${message}
|
|
@@ -474,7 +673,7 @@ function writeNoticeBox(title, lines, target = "stderr") {
|
|
|
474
673
|
}
|
|
475
674
|
writeLine(bottomBorder);
|
|
476
675
|
}
|
|
477
|
-
function formatTable(headers,
|
|
676
|
+
function formatTable(headers, rows2, target = "stdout") {
|
|
478
677
|
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
479
678
|
const useTuiBox = stream.isTTY;
|
|
480
679
|
const border = useTuiBox ? {
|
|
@@ -502,14 +701,26 @@ function formatTable(headers, rows, target = "stdout") {
|
|
|
502
701
|
middleJunction: "+",
|
|
503
702
|
bottomJunction: "+"
|
|
504
703
|
};
|
|
704
|
+
const getLines = (value) => (value ?? "").split("\n");
|
|
505
705
|
const widths = headers.map(
|
|
506
706
|
(header, index) => Math.max(
|
|
507
707
|
header.length,
|
|
508
|
-
...
|
|
708
|
+
...rows2.flatMap(
|
|
709
|
+
(row2) => getLines(row2[index]).map((line) => line.length)
|
|
710
|
+
)
|
|
509
711
|
)
|
|
510
712
|
);
|
|
511
713
|
const formatTableBorder = (left, join2, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join2)}${right}`;
|
|
512
|
-
const formatTableRow = (cells) =>
|
|
714
|
+
const formatTableRow = (cells) => {
|
|
715
|
+
const linesByCell = cells.map(getLines);
|
|
716
|
+
const rowHeight = Math.max(...linesByCell.map((lines) => lines.length));
|
|
717
|
+
return Array.from(
|
|
718
|
+
{ length: rowHeight },
|
|
719
|
+
(_value, rowIndex) => `${border.vertical}${linesByCell.map(
|
|
720
|
+
(lines, cellIndex) => ` ${(lines[rowIndex] ?? "").padEnd(widths[cellIndex])} `
|
|
721
|
+
).join(border.vertical)}${border.vertical}`
|
|
722
|
+
).join("\n");
|
|
723
|
+
};
|
|
513
724
|
return [
|
|
514
725
|
formatTableBorder(border.topLeft, border.topJunction, border.topRight),
|
|
515
726
|
formatTableRow(headers),
|
|
@@ -518,7 +729,7 @@ function formatTable(headers, rows, target = "stdout") {
|
|
|
518
729
|
border.middleJunction,
|
|
519
730
|
border.separatorRight
|
|
520
731
|
),
|
|
521
|
-
...
|
|
732
|
+
...rows2.map(formatTableRow),
|
|
522
733
|
formatTableBorder(
|
|
523
734
|
border.bottomLeft,
|
|
524
735
|
border.bottomJunction,
|
|
@@ -530,6 +741,261 @@ function writeJson(value) {
|
|
|
530
741
|
writeStdout(JSON.stringify(value, null, 2));
|
|
531
742
|
}
|
|
532
743
|
|
|
744
|
+
// src/lib/imports.ts
|
|
745
|
+
function text2(value) {
|
|
746
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
747
|
+
}
|
|
748
|
+
function normalizeHeader(value) {
|
|
749
|
+
return value.replace(/^\uFEFF/, "").trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
750
|
+
}
|
|
751
|
+
function parseCsvRows(text6) {
|
|
752
|
+
const rows2 = [];
|
|
753
|
+
const source = text6.replace(/^\uFEFF/, "");
|
|
754
|
+
let row2 = [];
|
|
755
|
+
let value = "";
|
|
756
|
+
let inQuotes = false;
|
|
757
|
+
const pushRow = () => {
|
|
758
|
+
row2.push(value);
|
|
759
|
+
if (row2.some((cell) => cell.trim() !== "")) {
|
|
760
|
+
rows2.push(row2);
|
|
761
|
+
}
|
|
762
|
+
row2 = [];
|
|
763
|
+
value = "";
|
|
764
|
+
};
|
|
765
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
766
|
+
const char = source[index];
|
|
767
|
+
if (char === '"') {
|
|
768
|
+
if (inQuotes && source[index + 1] === '"') {
|
|
769
|
+
value += '"';
|
|
770
|
+
index += 1;
|
|
771
|
+
} else {
|
|
772
|
+
inQuotes = !inQuotes;
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (char === "," && !inQuotes) {
|
|
777
|
+
row2.push(value);
|
|
778
|
+
value = "";
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if ((char === "\n" || char === "\r") && !inQuotes) {
|
|
782
|
+
if (char === "\r" && source[index + 1] === "\n") {
|
|
783
|
+
index += 1;
|
|
784
|
+
}
|
|
785
|
+
pushRow();
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
value += char;
|
|
789
|
+
}
|
|
790
|
+
if (value.length > 0 || row2.length > 0) {
|
|
791
|
+
pushRow();
|
|
792
|
+
}
|
|
793
|
+
return rows2;
|
|
794
|
+
}
|
|
795
|
+
function extractAlias(value) {
|
|
796
|
+
const input = value.trim();
|
|
797
|
+
if (!input) {
|
|
798
|
+
return void 0;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
const url = new URL(input);
|
|
802
|
+
const pathname = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
803
|
+
return pathname ? decodeURIComponent(pathname.split("/").pop() || "") : void 0;
|
|
804
|
+
} catch {
|
|
805
|
+
const pathname = input.replace(/^\/+|\/+$/g, "");
|
|
806
|
+
return pathname ? pathname.split("/").pop() || void 0 : void 0;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function decodeXml(value) {
|
|
810
|
+
return value.replace(/'/g, "'").replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&");
|
|
811
|
+
}
|
|
812
|
+
function normalizeImportRow(value) {
|
|
813
|
+
const destinationUrl = text2(value.destinationUrl) || text2(value.url) || text2(value.destination);
|
|
814
|
+
if (!destinationUrl) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
const alias = text2(value.alias) || text2(value.shortCode) || text2(value.shortcode) || text2(value.code) || extractAlias(
|
|
818
|
+
text2(value.shortUrl) || text2(value.short_url) || text2(value.shortLink) || text2(value.shortlink) || ""
|
|
819
|
+
);
|
|
820
|
+
return {
|
|
821
|
+
destinationUrl,
|
|
822
|
+
...alias ? { alias } : {},
|
|
823
|
+
...text2(value.title) ? { title: text2(value.title) } : {},
|
|
824
|
+
...text2(value.password) ? { password: text2(value.password) } : {},
|
|
825
|
+
...text2(value.status) ? { status: text2(value.status) } : {},
|
|
826
|
+
...text2(value.expiresAt) || text2(value.expires) ? { expiresAt: text2(value.expiresAt) || text2(value.expires) } : {},
|
|
827
|
+
...text2(value.utmSource) ? { utmSource: text2(value.utmSource) } : {},
|
|
828
|
+
...text2(value.utmMedium) ? { utmMedium: text2(value.utmMedium) } : {},
|
|
829
|
+
...text2(value.utmCampaign) ? { utmCampaign: text2(value.utmCampaign) } : {},
|
|
830
|
+
...text2(value.utmTerm) ? { utmTerm: text2(value.utmTerm) } : {},
|
|
831
|
+
...text2(value.utmContent) ? { utmContent: text2(value.utmContent) } : {}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function parseJson(text6) {
|
|
835
|
+
const parsed = JSON.parse(text6);
|
|
836
|
+
const items = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.urls) ? parsed.urls ?? [] : [];
|
|
837
|
+
return items.map(
|
|
838
|
+
(item) => item && typeof item === "object" ? normalizeImportRow(item) : null
|
|
839
|
+
).filter((item) => Boolean(item));
|
|
840
|
+
}
|
|
841
|
+
function parseCsv(text6) {
|
|
842
|
+
const rows2 = parseCsvRows(text6);
|
|
843
|
+
if (rows2.length < 2) {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
const headers = rows2[0].map((header) => normalizeHeader(header));
|
|
847
|
+
const links = [];
|
|
848
|
+
for (let index = 1; index < rows2.length; index += 1) {
|
|
849
|
+
const row2 = rows2[index];
|
|
850
|
+
const entry = {};
|
|
851
|
+
headers.forEach((header, column) => {
|
|
852
|
+
const value = row2[column]?.trim();
|
|
853
|
+
if (!value) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (header === "url" || header === "destinationurl" || header === "destination") {
|
|
857
|
+
entry.destinationUrl = value;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (header === "alias" || header === "shortcode" || header === "code") {
|
|
861
|
+
entry.alias = value;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (header === "shorturl" || header === "shortlink") {
|
|
865
|
+
entry.alias = entry.alias || extractAlias(value);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (header === "password") {
|
|
869
|
+
entry.password = value;
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (header === "expires" || header === "expiresat") {
|
|
873
|
+
entry.expiresAt = value;
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (header === "title") {
|
|
877
|
+
entry.title = value;
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (header === "status") {
|
|
881
|
+
entry.status = value;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (header === "utmsource") {
|
|
885
|
+
entry.utmSource = value;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (header === "utmmedium") {
|
|
889
|
+
entry.utmMedium = value;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (header === "utmcampaign") {
|
|
893
|
+
entry.utmCampaign = value;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (header === "utmterm") {
|
|
897
|
+
entry.utmTerm = value;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (header === "utmcontent") {
|
|
901
|
+
entry.utmContent = value;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
const link = normalizeImportRow(entry);
|
|
905
|
+
if (link) {
|
|
906
|
+
links.push(link);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return links;
|
|
910
|
+
}
|
|
911
|
+
function parseXml(text6) {
|
|
912
|
+
const entries = Array.from(
|
|
913
|
+
text6.matchAll(/<(url|item)\b[^>]*>([\s\S]*?)<\/\1>/gi),
|
|
914
|
+
(match) => match[2]
|
|
915
|
+
);
|
|
916
|
+
return entries.map((entry) => {
|
|
917
|
+
const getValue = (tag) => {
|
|
918
|
+
const match = new RegExp(
|
|
919
|
+
`<${tag}>([\\s\\S]*?)</${tag}>`,
|
|
920
|
+
"i"
|
|
921
|
+
).exec(entry);
|
|
922
|
+
return match ? decodeXml(match[1].trim()) : void 0;
|
|
923
|
+
};
|
|
924
|
+
return normalizeImportRow({
|
|
925
|
+
destinationUrl: getValue("destinationUrl"),
|
|
926
|
+
url: getValue("url"),
|
|
927
|
+
alias: getValue("alias"),
|
|
928
|
+
shortCode: getValue("shortCode"),
|
|
929
|
+
shortUrl: getValue("shortUrl"),
|
|
930
|
+
password: getValue("password"),
|
|
931
|
+
expiresAt: getValue("expiresAt"),
|
|
932
|
+
expires: getValue("expires"),
|
|
933
|
+
title: getValue("title"),
|
|
934
|
+
status: getValue("status"),
|
|
935
|
+
utmSource: getValue("utmSource"),
|
|
936
|
+
utmMedium: getValue("utmMedium"),
|
|
937
|
+
utmCampaign: getValue("utmCampaign"),
|
|
938
|
+
utmTerm: getValue("utmTerm"),
|
|
939
|
+
utmContent: getValue("utmContent")
|
|
940
|
+
});
|
|
941
|
+
}).filter((item) => Boolean(item));
|
|
942
|
+
}
|
|
943
|
+
function getImportFormat(filePath, value) {
|
|
944
|
+
if (value) {
|
|
945
|
+
return parseFileFormat(value);
|
|
946
|
+
}
|
|
947
|
+
const format = getFileFormatFromPath(filePath);
|
|
948
|
+
if (format) {
|
|
949
|
+
return format;
|
|
950
|
+
}
|
|
951
|
+
throw new CliError(
|
|
952
|
+
"Could not determine the import file format. Use a .csv, .json, or .xml file, or pass --format."
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
async function readImportRows(filePath, format) {
|
|
956
|
+
let textContent = "";
|
|
957
|
+
try {
|
|
958
|
+
textContent = await readFile2(filePath, "utf8");
|
|
959
|
+
} catch (error) {
|
|
960
|
+
throw new CliError(`Could not read import file ${filePath}.`, 1, {
|
|
961
|
+
cause: error instanceof Error ? error : void 0
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const rows2 = format === "json" ? parseJson(textContent) : format === "xml" ? parseXml(textContent) : parseCsv(textContent);
|
|
966
|
+
if (rows2.length === 0) {
|
|
967
|
+
throw new CliError(`No import rows were found in ${filePath}.`);
|
|
968
|
+
}
|
|
969
|
+
return rows2;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (error instanceof CliError) {
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
throw new CliError(
|
|
975
|
+
`Could not parse ${filePath} as ${format.toUpperCase()}.`,
|
|
976
|
+
1,
|
|
977
|
+
{
|
|
978
|
+
cause: error instanceof Error ? error : void 0
|
|
979
|
+
}
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function formatImportErrors(errors) {
|
|
984
|
+
return formatTable(
|
|
985
|
+
["Destination", "Alias", "Error"],
|
|
986
|
+
errors.map((item) => [
|
|
987
|
+
item.destinationUrl || "-",
|
|
988
|
+
item.alias || "-",
|
|
989
|
+
item.error || "Unknown error"
|
|
990
|
+
])
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
function formatImportSummary(data) {
|
|
994
|
+
const created = data.results?.length ?? 0;
|
|
995
|
+
const errors = data.errors?.length ?? 0;
|
|
996
|
+
return `${created} link${created === 1 ? "" : "s"} created. ${errors} error${errors === 1 ? "" : "s"}.`;
|
|
997
|
+
}
|
|
998
|
+
|
|
533
999
|
// src/lib/links.ts
|
|
534
1000
|
var LIST_KEYS = ["urls", "items", "results"];
|
|
535
1001
|
function asObject(value) {
|
|
@@ -631,14 +1097,14 @@ function formatLinksTable(links) {
|
|
|
631
1097
|
return "No links found.";
|
|
632
1098
|
}
|
|
633
1099
|
const headers = ["ID", "Alias", "Short URL", "Destination", "Status"];
|
|
634
|
-
const
|
|
1100
|
+
const rows2 = links.map((link) => [
|
|
635
1101
|
truncate(getLinkId(link) || "-", 18),
|
|
636
1102
|
truncate(getLinkAlias(link) || "-", 12),
|
|
637
1103
|
truncate(getLinkShortUrl(link) || "-", 36),
|
|
638
1104
|
truncate(getLinkDestination(link) || "-", 52),
|
|
639
1105
|
truncate(asString(link.status) || "-", 12)
|
|
640
1106
|
]);
|
|
641
|
-
return formatTable(headers,
|
|
1107
|
+
return formatTable(headers, rows2);
|
|
642
1108
|
}
|
|
643
1109
|
function formatListSummary(data, count) {
|
|
644
1110
|
const meta = getListMeta(data);
|
|
@@ -654,6 +1120,323 @@ function formatListSummary(data, count) {
|
|
|
654
1120
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
655
1121
|
}
|
|
656
1122
|
|
|
1123
|
+
// src/lib/status.ts
|
|
1124
|
+
function text3(value) {
|
|
1125
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1126
|
+
}
|
|
1127
|
+
function integer(value) {
|
|
1128
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1129
|
+
return value;
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof value === "string" && value.trim()) {
|
|
1132
|
+
const parsed = Number(value);
|
|
1133
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1134
|
+
}
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
function flag(value) {
|
|
1138
|
+
if (typeof value === "boolean") {
|
|
1139
|
+
return value;
|
|
1140
|
+
}
|
|
1141
|
+
if (value === 1 || value === "1") {
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
if (value === 0 || value === "0") {
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
return void 0;
|
|
1148
|
+
}
|
|
1149
|
+
function yesNo(value, yes = "Yes", no = "No") {
|
|
1150
|
+
const normalized = flag(value);
|
|
1151
|
+
if (normalized === void 0) {
|
|
1152
|
+
return void 0;
|
|
1153
|
+
}
|
|
1154
|
+
return normalized ? yes : no;
|
|
1155
|
+
}
|
|
1156
|
+
function formatState(value) {
|
|
1157
|
+
const normalized = text3(value)?.toLowerCase();
|
|
1158
|
+
if (!normalized) {
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
switch (normalized) {
|
|
1162
|
+
case "ok":
|
|
1163
|
+
return "Good";
|
|
1164
|
+
case "warning":
|
|
1165
|
+
return "Warning";
|
|
1166
|
+
case "error":
|
|
1167
|
+
return "Error";
|
|
1168
|
+
default:
|
|
1169
|
+
return normalized;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function formatCount(value) {
|
|
1173
|
+
const normalized = integer(value);
|
|
1174
|
+
return normalized === void 0 ? text3(value) : String(normalized);
|
|
1175
|
+
}
|
|
1176
|
+
function formatSize(value) {
|
|
1177
|
+
const bytes = integer(value);
|
|
1178
|
+
if (bytes === void 0) {
|
|
1179
|
+
return text3(value);
|
|
1180
|
+
}
|
|
1181
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
1182
|
+
let size = bytes;
|
|
1183
|
+
let unitIndex = 0;
|
|
1184
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
1185
|
+
size /= 1024;
|
|
1186
|
+
unitIndex += 1;
|
|
1187
|
+
}
|
|
1188
|
+
const amount = size >= 10 || unitIndex === 0 ? size.toFixed(0) : size.toFixed(1);
|
|
1189
|
+
return `${amount} ${units[unitIndex]}`;
|
|
1190
|
+
}
|
|
1191
|
+
function formatSeconds(value) {
|
|
1192
|
+
const normalized = integer(value);
|
|
1193
|
+
return normalized === void 0 ? text3(value) : `${normalized} seconds`;
|
|
1194
|
+
}
|
|
1195
|
+
function wrapText(value, width = 68) {
|
|
1196
|
+
const lines = [];
|
|
1197
|
+
for (const line of value.split("\n")) {
|
|
1198
|
+
if (line.length <= width) {
|
|
1199
|
+
lines.push(line);
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
for (let index = 0; index < line.length; index += width) {
|
|
1203
|
+
lines.push(line.slice(index, index + width));
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return lines.join("\n");
|
|
1207
|
+
}
|
|
1208
|
+
function row(label, value, width) {
|
|
1209
|
+
const normalized = typeof value === "string" ? value : typeof value === "number" ? String(value) : void 0;
|
|
1210
|
+
if (!normalized) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
return [label, wrapText(normalized, width)];
|
|
1214
|
+
}
|
|
1215
|
+
function rows(values) {
|
|
1216
|
+
return values.filter((value) => Boolean(value));
|
|
1217
|
+
}
|
|
1218
|
+
function getSummary(status2) {
|
|
1219
|
+
const summary = status2.summary ?? {};
|
|
1220
|
+
const checks = status2.checks ?? [];
|
|
1221
|
+
if (summary.okCount !== void 0 && summary.warningCount !== void 0 && summary.errorCount !== void 0 && summary.totalChecks !== void 0) {
|
|
1222
|
+
return summary;
|
|
1223
|
+
}
|
|
1224
|
+
let okCount = 0;
|
|
1225
|
+
let warningCount = 0;
|
|
1226
|
+
let errorCount = 0;
|
|
1227
|
+
for (const check of checks) {
|
|
1228
|
+
switch (text3(check.status)?.toLowerCase()) {
|
|
1229
|
+
case "error":
|
|
1230
|
+
errorCount += 1;
|
|
1231
|
+
break;
|
|
1232
|
+
case "warning":
|
|
1233
|
+
warningCount += 1;
|
|
1234
|
+
break;
|
|
1235
|
+
case "ok":
|
|
1236
|
+
okCount += 1;
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
overall: summary.overall ?? (errorCount > 0 ? "error" : warningCount > 0 ? "warning" : "ok"),
|
|
1242
|
+
okCount: summary.okCount ?? okCount,
|
|
1243
|
+
warningCount: summary.warningCount ?? warningCount,
|
|
1244
|
+
errorCount: summary.errorCount ?? errorCount,
|
|
1245
|
+
totalChecks: summary.totalChecks ?? checks.length
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function summaryRows(status2) {
|
|
1249
|
+
const summary = getSummary(status2);
|
|
1250
|
+
return rows([
|
|
1251
|
+
row("Overall", formatState(summary.overall)),
|
|
1252
|
+
row("Generated at", text3(status2.generatedAt)),
|
|
1253
|
+
row("Total checks", formatCount(summary.totalChecks)),
|
|
1254
|
+
row("Good checks", formatCount(summary.okCount)),
|
|
1255
|
+
row("Warnings", formatCount(summary.warningCount)),
|
|
1256
|
+
row("Errors", formatCount(summary.errorCount))
|
|
1257
|
+
]);
|
|
1258
|
+
}
|
|
1259
|
+
function siteRows(site) {
|
|
1260
|
+
if (!site) {
|
|
1261
|
+
return [];
|
|
1262
|
+
}
|
|
1263
|
+
return rows([
|
|
1264
|
+
row("Name", text3(site.name)),
|
|
1265
|
+
row("URL", text3(site.url)),
|
|
1266
|
+
row("Version", text3(site.version)),
|
|
1267
|
+
row("Environment", text3(site.environment)),
|
|
1268
|
+
row("Install type", text3(site.installType)),
|
|
1269
|
+
row("Locale", text3(site.locale)),
|
|
1270
|
+
row("HTML lang", text3(site.htmlLang)),
|
|
1271
|
+
row(
|
|
1272
|
+
"Language",
|
|
1273
|
+
text3(site.languageNativeName) || text3(site.languageLabel)
|
|
1274
|
+
),
|
|
1275
|
+
row("Installed languages", formatCount(site.installedLanguagesCount)),
|
|
1276
|
+
row("Default locale", text3(site.defaultLocale)),
|
|
1277
|
+
row("Debug", yesNo(site.debugEnabled, "Enabled", "Disabled"))
|
|
1278
|
+
]);
|
|
1279
|
+
}
|
|
1280
|
+
function serverRows(server) {
|
|
1281
|
+
if (!server) {
|
|
1282
|
+
return [];
|
|
1283
|
+
}
|
|
1284
|
+
return rows([
|
|
1285
|
+
row("PHP version", text3(server.phpVersion)),
|
|
1286
|
+
row("PHP SAPI", text3(server.phpSapi)),
|
|
1287
|
+
row("Server software", text3(server.serverSoftware), 56),
|
|
1288
|
+
row("Operating system", text3(server.operatingSystem)),
|
|
1289
|
+
row("Timezone", text3(server.timezone)),
|
|
1290
|
+
row("Memory limit", text3(server.memoryLimit)),
|
|
1291
|
+
row("Max execution time", formatSeconds(server.maxExecutionTime)),
|
|
1292
|
+
row("Upload max filesize", text3(server.uploadMaxFilesize)),
|
|
1293
|
+
row("Post max size", text3(server.postMaxSize)),
|
|
1294
|
+
row("Intl extension", yesNo(server.extensions?.intl)),
|
|
1295
|
+
row("cURL extension", yesNo(server.extensions?.curl)),
|
|
1296
|
+
row("ZipArchive", yesNo(server.extensions?.zip))
|
|
1297
|
+
]);
|
|
1298
|
+
}
|
|
1299
|
+
function databaseRows(database) {
|
|
1300
|
+
if (!database) {
|
|
1301
|
+
return [];
|
|
1302
|
+
}
|
|
1303
|
+
return rows([
|
|
1304
|
+
row("Connected", yesNo(database.connected)),
|
|
1305
|
+
row("Server type", text3(database.serverType)),
|
|
1306
|
+
row("Version", text3(database.version)),
|
|
1307
|
+
row("Host", text3(database.host)),
|
|
1308
|
+
row("Port", formatCount(database.port)),
|
|
1309
|
+
row("Name", text3(database.name)),
|
|
1310
|
+
row("Charset", text3(database.charset)),
|
|
1311
|
+
row("Prefix", text3(database.prefix)),
|
|
1312
|
+
row("Schema version", formatCount(database.schemaVersion)),
|
|
1313
|
+
row(
|
|
1314
|
+
"Required schema version",
|
|
1315
|
+
formatCount(database.requiredSchemaVersion)
|
|
1316
|
+
),
|
|
1317
|
+
row("Schema compatible", yesNo(database.schemaCompatible)),
|
|
1318
|
+
row("Schema upgrade required", yesNo(database.schemaUpgradeRequired)),
|
|
1319
|
+
row("Schema issues", formatCount(database.schemaIssuesCount)),
|
|
1320
|
+
row("Last upgraded", text3(database.schemaLastUpgradedAt)),
|
|
1321
|
+
row("Last error", text3(database.schemaLastError), 56)
|
|
1322
|
+
]);
|
|
1323
|
+
}
|
|
1324
|
+
function storageRows(storage) {
|
|
1325
|
+
if (!storage) {
|
|
1326
|
+
return [];
|
|
1327
|
+
}
|
|
1328
|
+
return rows([
|
|
1329
|
+
row("Content directory", text3(storage.contentDirectory), 60),
|
|
1330
|
+
row("Content exists", yesNo(storage.contentExists)),
|
|
1331
|
+
row("Content writable", yesNo(storage.contentWritable)),
|
|
1332
|
+
row("Content size", formatSize(storage.contentDirectorySizeBytes)),
|
|
1333
|
+
row("Languages directory", text3(storage.languagesDirectory), 60),
|
|
1334
|
+
row("Languages exists", yesNo(storage.languagesDirectoryExists)),
|
|
1335
|
+
row("Languages readable", yesNo(storage.languagesDirectoryReadable)),
|
|
1336
|
+
row("Languages size", formatSize(storage.languagesDirectorySizeBytes)),
|
|
1337
|
+
row("Config path", text3(storage.configPath), 60),
|
|
1338
|
+
row("Config exists", yesNo(storage.configExists)),
|
|
1339
|
+
row("Config size", formatSize(storage.configSizeBytes)),
|
|
1340
|
+
row("Debug log path", text3(storage.debugLogPath), 60),
|
|
1341
|
+
row("Debug log exists", yesNo(storage.debugLogExists)),
|
|
1342
|
+
row("Debug log readable", yesNo(storage.debugLogReadable)),
|
|
1343
|
+
row("Debug log size", formatSize(storage.debugLogSizeBytes)),
|
|
1344
|
+
row("App directory", text3(storage.appDirectory), 60),
|
|
1345
|
+
row("App writable", yesNo(storage.appWritable)),
|
|
1346
|
+
row("App size", formatSize(storage.appDirectorySizeBytes)),
|
|
1347
|
+
row("Release root", text3(storage.releaseRoot), 60),
|
|
1348
|
+
row("Release size", formatSize(storage.releaseRootSizeBytes))
|
|
1349
|
+
]);
|
|
1350
|
+
}
|
|
1351
|
+
function mailRows(mail) {
|
|
1352
|
+
if (!mail) {
|
|
1353
|
+
return [];
|
|
1354
|
+
}
|
|
1355
|
+
return rows([
|
|
1356
|
+
row("Driver", text3(mail.driver)),
|
|
1357
|
+
row("Transport", yesNo(mail.transportReady, "Ready", "Not ready")),
|
|
1358
|
+
row("From email", text3(mail.fromEmail)),
|
|
1359
|
+
row("From name", text3(mail.fromName)),
|
|
1360
|
+
row("SMTP host", text3(mail.smtpHost)),
|
|
1361
|
+
row("SMTP port", text3(mail.smtpPort)),
|
|
1362
|
+
row("SMTP encryption", text3(mail.smtpEncryption)),
|
|
1363
|
+
row("SMTP auth", yesNo(mail.smtpAuth)),
|
|
1364
|
+
row("Configuration", text3(mail.configurationLabel)),
|
|
1365
|
+
row("Configuration path", text3(mail.configurationPath), 60)
|
|
1366
|
+
]);
|
|
1367
|
+
}
|
|
1368
|
+
function locationRows(location) {
|
|
1369
|
+
if (!location) {
|
|
1370
|
+
return [];
|
|
1371
|
+
}
|
|
1372
|
+
return rows([
|
|
1373
|
+
row(
|
|
1374
|
+
"Analytics",
|
|
1375
|
+
yesNo(location.locationAnalyticsReady, "Ready", "Not ready")
|
|
1376
|
+
),
|
|
1377
|
+
row("Last downloaded", text3(location.lastDownloadedAt)),
|
|
1378
|
+
row("Database updated", text3(location.databaseUpdatedAt)),
|
|
1379
|
+
row("Database size", formatSize(location.databaseSizeBytes)),
|
|
1380
|
+
row("Credentials configured", yesNo(location.credentialsConfigured)),
|
|
1381
|
+
row("Account ID", text3(location.accountId)),
|
|
1382
|
+
row("Database path", text3(location.databasePath), 60),
|
|
1383
|
+
row("Database readable", yesNo(location.databaseReadable)),
|
|
1384
|
+
row("Download command", text3(location.downloadCommand), 60)
|
|
1385
|
+
]);
|
|
1386
|
+
}
|
|
1387
|
+
function dataRows(data) {
|
|
1388
|
+
if (!data) {
|
|
1389
|
+
return [];
|
|
1390
|
+
}
|
|
1391
|
+
return rows([
|
|
1392
|
+
row("Users", formatCount(data.users)),
|
|
1393
|
+
row("Links", formatCount(data.links)),
|
|
1394
|
+
row("Clicks", formatCount(data.clicks)),
|
|
1395
|
+
row("Sessions", formatCount(data.sessions)),
|
|
1396
|
+
row("API keys", formatCount(data.apiKeys)),
|
|
1397
|
+
row("Webhooks", formatCount(data.webhooks)),
|
|
1398
|
+
row("Audit events", formatCount(data.auditEvents)),
|
|
1399
|
+
row("Managed tables", formatCount(data.managedTables))
|
|
1400
|
+
]);
|
|
1401
|
+
}
|
|
1402
|
+
function checksTable(checks) {
|
|
1403
|
+
if (checks.length === 0) {
|
|
1404
|
+
return void 0;
|
|
1405
|
+
}
|
|
1406
|
+
const rows2 = checks.map((check) => [
|
|
1407
|
+
wrapText(text3(check.label) || "Check", 24),
|
|
1408
|
+
formatState(check.status) || "Unknown",
|
|
1409
|
+
wrapText(text3(check.description) || "Not available", 56)
|
|
1410
|
+
]);
|
|
1411
|
+
return formatTable(["Check", "Status", "Description"], rows2);
|
|
1412
|
+
}
|
|
1413
|
+
function section(title, rows2) {
|
|
1414
|
+
if (rows2.length === 0) {
|
|
1415
|
+
return void 0;
|
|
1416
|
+
}
|
|
1417
|
+
return `${title}
|
|
1418
|
+
${formatTable(["Field", "Value"], rows2)}`;
|
|
1419
|
+
}
|
|
1420
|
+
function getStatusValue(status2) {
|
|
1421
|
+
return text3(getSummary(status2).overall) || "unknown";
|
|
1422
|
+
}
|
|
1423
|
+
function formatStatusReport(status2) {
|
|
1424
|
+
const checks = checksTable(status2.checks ?? []);
|
|
1425
|
+
const sections = [
|
|
1426
|
+
section("Summary", summaryRows(status2)),
|
|
1427
|
+
checks ? `Checks
|
|
1428
|
+
${checks}` : void 0,
|
|
1429
|
+
section("Site", siteRows(status2.site)),
|
|
1430
|
+
section("Server", serverRows(status2.server)),
|
|
1431
|
+
section("Database", databaseRows(status2.database)),
|
|
1432
|
+
section("Storage", storageRows(status2.storage)),
|
|
1433
|
+
section("Mail", mailRows(status2.mail)),
|
|
1434
|
+
section("Location", locationRows(status2.location)),
|
|
1435
|
+
section("Data", dataRows(status2.data))
|
|
1436
|
+
].filter((value) => Boolean(value));
|
|
1437
|
+
return sections.length > 0 ? sections.join("\n\n") : "No system status fields returned.";
|
|
1438
|
+
}
|
|
1439
|
+
|
|
657
1440
|
// src/lib/update.ts
|
|
658
1441
|
var PACKAGE_NAME = "peakurl";
|
|
659
1442
|
var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -829,10 +1612,10 @@ async function getUpdateStatus(currentVersion, env, options) {
|
|
|
829
1612
|
installCommand: getUpdateInstallCommand()
|
|
830
1613
|
};
|
|
831
1614
|
}
|
|
832
|
-
function showUpdateNotice(
|
|
1615
|
+
function showUpdateNotice(status2) {
|
|
833
1616
|
writeNoticeBox("Update Available", [
|
|
834
|
-
`${PACKAGE_NAME} ${
|
|
835
|
-
`Run: ${
|
|
1617
|
+
`${PACKAGE_NAME} ${status2.currentVersion} -> ${status2.latestVersion}`,
|
|
1618
|
+
`Run: ${status2.installCommand}`
|
|
836
1619
|
]);
|
|
837
1620
|
}
|
|
838
1621
|
async function checkUpdates(options) {
|
|
@@ -844,51 +1627,157 @@ async function checkUpdates(options) {
|
|
|
844
1627
|
}
|
|
845
1628
|
const store = new StateStore();
|
|
846
1629
|
const updateState = await getUpdateState(store);
|
|
847
|
-
const
|
|
1630
|
+
const status2 = await getUpdateStatus(options.currentVersion, options.env, {
|
|
848
1631
|
store
|
|
849
1632
|
});
|
|
850
|
-
if (!
|
|
1633
|
+
if (!status2.isOutdated) {
|
|
851
1634
|
return;
|
|
852
1635
|
}
|
|
853
1636
|
const lastNotifiedAt = parseTime(updateState.lastNotifiedAt);
|
|
854
|
-
const alreadyNotifiedForVersion = updateState.lastNotifiedVersion ===
|
|
1637
|
+
const alreadyNotifiedForVersion = updateState.lastNotifiedVersion === status2.latestVersion;
|
|
855
1638
|
if (alreadyNotifiedForVersion && lastNotifiedAt !== null && Date.now() - lastNotifiedAt < NOTICE_TTL_MS) {
|
|
856
1639
|
return;
|
|
857
1640
|
}
|
|
858
1641
|
await saveUpdateState(store, {
|
|
859
1642
|
...updateState,
|
|
860
1643
|
lastNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
861
|
-
lastNotifiedVersion:
|
|
1644
|
+
lastNotifiedVersion: status2.latestVersion
|
|
862
1645
|
});
|
|
863
|
-
showUpdateNotice(
|
|
1646
|
+
showUpdateNotice(status2);
|
|
864
1647
|
}
|
|
865
1648
|
|
|
866
1649
|
// src/lib/users.ts
|
|
867
|
-
function
|
|
1650
|
+
function text4(value) {
|
|
868
1651
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
869
1652
|
}
|
|
870
1653
|
function userLabel(user) {
|
|
871
|
-
const fullName = [
|
|
872
|
-
return fullName ||
|
|
1654
|
+
const fullName = [text4(user.firstName), text4(user.lastName)].filter((value) => Boolean(value)).join(" ");
|
|
1655
|
+
return fullName || text4(user.username) || text4(user.email) || String(user.id ?? "unknown");
|
|
873
1656
|
}
|
|
874
1657
|
function userValue(user) {
|
|
875
|
-
return
|
|
1658
|
+
return text4(user.username) || text4(user.email) || String(user.id ?? "");
|
|
876
1659
|
}
|
|
877
1660
|
function userTable(user) {
|
|
878
|
-
const
|
|
1661
|
+
const rows2 = [
|
|
879
1662
|
["Name", userLabel(user)],
|
|
880
|
-
["Username",
|
|
881
|
-
["Email",
|
|
882
|
-
["Role",
|
|
1663
|
+
["Username", text4(user.username)],
|
|
1664
|
+
["Email", text4(user.email)],
|
|
1665
|
+
["Role", text4(user.role)],
|
|
883
1666
|
["ID", user.id === void 0 ? void 0 : String(user.id)]
|
|
884
1667
|
].filter((entry) => Boolean(entry[1]));
|
|
885
|
-
if (
|
|
1668
|
+
if (rows2.length === 0) {
|
|
886
1669
|
return "No user fields returned.";
|
|
887
1670
|
}
|
|
888
|
-
return formatTable(["Field", "Value"],
|
|
1671
|
+
return formatTable(["Field", "Value"], rows2);
|
|
889
1672
|
}
|
|
890
1673
|
|
|
891
|
-
// src/
|
|
1674
|
+
// src/lib/webhooks.ts
|
|
1675
|
+
var WEBHOOK_EVENTS = [
|
|
1676
|
+
{
|
|
1677
|
+
id: "link.created",
|
|
1678
|
+
label: "Link Created",
|
|
1679
|
+
description: "Send a delivery when a short link is created."
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
id: "link.clicked",
|
|
1683
|
+
label: "Link Clicked",
|
|
1684
|
+
description: "Send a delivery when a visitor clicks a short link."
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
id: "link.updated",
|
|
1688
|
+
label: "Link Updated",
|
|
1689
|
+
description: "Send a delivery when a short link is updated."
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
id: "link.deleted",
|
|
1693
|
+
label: "Link Deleted",
|
|
1694
|
+
description: "Send a delivery when a short link is deleted."
|
|
1695
|
+
}
|
|
1696
|
+
];
|
|
1697
|
+
function text5(value) {
|
|
1698
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1699
|
+
}
|
|
1700
|
+
function textList(value) {
|
|
1701
|
+
return Array.isArray(value) ? value.map((item) => text5(item)).filter((item) => Boolean(item)) : [];
|
|
1702
|
+
}
|
|
1703
|
+
function truncate2(value, maxLength) {
|
|
1704
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
1705
|
+
}
|
|
1706
|
+
function parseWebhookEvents(value, previous = []) {
|
|
1707
|
+
const allowed = new Set(WEBHOOK_EVENTS.map((event) => event.id));
|
|
1708
|
+
const values = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
1709
|
+
if (values.length === 0) {
|
|
1710
|
+
throw new CliError("A webhook event value is required.");
|
|
1711
|
+
}
|
|
1712
|
+
for (const event of values) {
|
|
1713
|
+
if (!allowed.has(event)) {
|
|
1714
|
+
throw new CliError(
|
|
1715
|
+
`Unknown webhook event: ${event}. Run \`peakurl webhook events\` to see the supported values.`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return [...previous, ...values];
|
|
1720
|
+
}
|
|
1721
|
+
function getWebhookId(webhook) {
|
|
1722
|
+
return text5(webhook.id) || (webhook.id !== void 0 ? String(webhook.id) : void 0);
|
|
1723
|
+
}
|
|
1724
|
+
function getWebhookUrl(webhook) {
|
|
1725
|
+
return text5(webhook.url);
|
|
1726
|
+
}
|
|
1727
|
+
function getWebhookEvents(webhook) {
|
|
1728
|
+
return textList(webhook.events);
|
|
1729
|
+
}
|
|
1730
|
+
function getQuietWebhookValue(webhook) {
|
|
1731
|
+
return getWebhookId(webhook) || getWebhookUrl(webhook) || "";
|
|
1732
|
+
}
|
|
1733
|
+
function formatWebhookEventsTable() {
|
|
1734
|
+
return formatTable(
|
|
1735
|
+
["Event", "Label", "Description"],
|
|
1736
|
+
WEBHOOK_EVENTS.map((event) => [
|
|
1737
|
+
event.id,
|
|
1738
|
+
event.label,
|
|
1739
|
+
event.description
|
|
1740
|
+
])
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
function formatWebhooksTable(webhooks) {
|
|
1744
|
+
if (webhooks.length === 0) {
|
|
1745
|
+
return "No webhooks found.";
|
|
1746
|
+
}
|
|
1747
|
+
return formatTable(
|
|
1748
|
+
["ID", "URL", "Events", "Status", "Secret"],
|
|
1749
|
+
webhooks.map((webhook) => [
|
|
1750
|
+
truncate2(getWebhookId(webhook) || "-", 18),
|
|
1751
|
+
truncate2(getWebhookUrl(webhook) || "-", 42),
|
|
1752
|
+
truncate2(getWebhookEvents(webhook).join(", ") || "-", 30),
|
|
1753
|
+
webhook.isActive === false ? "inactive" : "active",
|
|
1754
|
+
truncate2(text5(webhook.secretHint) || "-", 18)
|
|
1755
|
+
])
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
function formatWebhookDetails(webhook) {
|
|
1759
|
+
const rows2 = [
|
|
1760
|
+
["ID", getWebhookId(webhook)],
|
|
1761
|
+
["URL", getWebhookUrl(webhook)],
|
|
1762
|
+
[
|
|
1763
|
+
"Events",
|
|
1764
|
+
getWebhookEvents(webhook).length > 0 ? getWebhookEvents(webhook).join(", ") : void 0
|
|
1765
|
+
],
|
|
1766
|
+
["Status", webhook.isActive === false ? "inactive" : "active"],
|
|
1767
|
+
["Secret", text5(webhook.secret)],
|
|
1768
|
+
["Secret Hint", text5(webhook.secretHint)],
|
|
1769
|
+
["Created", text5(webhook.createdAt)]
|
|
1770
|
+
].filter((entry) => Boolean(entry[1]));
|
|
1771
|
+
if (rows2.length === 0) {
|
|
1772
|
+
return "No webhook fields returned.";
|
|
1773
|
+
}
|
|
1774
|
+
return formatTable(["Field", "Value"], rows2);
|
|
1775
|
+
}
|
|
1776
|
+
function formatWebhooksSummary(webhooks) {
|
|
1777
|
+
return `${webhooks.length} webhook${webhooks.length === 1 ? "" : "s"} returned.`;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/commands/links.ts
|
|
892
1781
|
function normalizeExpiresAt(value) {
|
|
893
1782
|
if (!value) {
|
|
894
1783
|
return void 0;
|
|
@@ -898,7 +1787,7 @@ function normalizeExpiresAt(value) {
|
|
|
898
1787
|
}
|
|
899
1788
|
return value;
|
|
900
1789
|
}
|
|
901
|
-
async function
|
|
1790
|
+
async function createLink(destinationUrl, options) {
|
|
902
1791
|
const config = await getAuthConfig(process.env);
|
|
903
1792
|
const response = await new ApiClient(config).createUrl({
|
|
904
1793
|
destinationUrl: normalizeDestinationUrl(destinationUrl),
|
|
@@ -924,47 +1813,73 @@ async function createCommand(destinationUrl, options) {
|
|
|
924
1813
|
writeStdout(response.message);
|
|
925
1814
|
writeStdout(formatLinkDetails(response.data));
|
|
926
1815
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1816
|
+
async function importLinks(filePath, options) {
|
|
1817
|
+
const format = getImportFormat(filePath, options.format);
|
|
1818
|
+
const urls = await readImportRows(filePath, format);
|
|
930
1819
|
const config = await getAuthConfig(process.env);
|
|
931
|
-
const
|
|
932
|
-
const
|
|
933
|
-
const
|
|
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);
|
|
1820
|
+
const response = await new ApiClient(config).importUrls({ urls });
|
|
1821
|
+
const links = extractLinks(response.data?.results ?? []);
|
|
1822
|
+
const errors = response.data?.errors ?? [];
|
|
940
1823
|
if (options.json) {
|
|
941
1824
|
writeJson(response);
|
|
942
1825
|
return;
|
|
943
1826
|
}
|
|
944
1827
|
if (options.quiet) {
|
|
1828
|
+
for (const link of links) {
|
|
1829
|
+
const value = getQuietLinkValue(link);
|
|
1830
|
+
if (value) {
|
|
1831
|
+
writeStdout(value);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
945
1834
|
return;
|
|
946
1835
|
}
|
|
947
1836
|
writeStdout(response.message);
|
|
1837
|
+
if (links.length > 0) {
|
|
1838
|
+
writeStdout(formatLinksTable(links));
|
|
1839
|
+
} else {
|
|
1840
|
+
writeStdout("No links were created.");
|
|
1841
|
+
}
|
|
1842
|
+
if (errors.length > 0) {
|
|
1843
|
+
writeStdout(formatImportErrors(errors));
|
|
1844
|
+
}
|
|
1845
|
+
writeStdout(formatImportSummary(response.data ?? {}));
|
|
948
1846
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1847
|
+
async function exportLinks(options) {
|
|
1848
|
+
if (options.stdout && options.output) {
|
|
1849
|
+
throw new CliError("Use either --stdout or --output, not both.");
|
|
1850
|
+
}
|
|
1851
|
+
const format = options.format ? parseFileFormat(options.format) : options.output ? getFileFormatFromPath(options.output) || "csv" : "csv";
|
|
952
1852
|
const config = await getAuthConfig(process.env);
|
|
953
|
-
const response = await new ApiClient(config).
|
|
954
|
-
|
|
955
|
-
|
|
1853
|
+
const response = await new ApiClient(config).exportUrls({
|
|
1854
|
+
search: options.search,
|
|
1855
|
+
sortBy: options.sortBy,
|
|
1856
|
+
sortOrder: options.sortOrder
|
|
1857
|
+
});
|
|
1858
|
+
const links = extractLinks(response.data);
|
|
1859
|
+
const content = serializeLinkExport(links, format);
|
|
1860
|
+
if (options.stdout) {
|
|
1861
|
+
process.stdout.write(content);
|
|
956
1862
|
return;
|
|
957
1863
|
}
|
|
1864
|
+
const filePath = resolve(options.output || getExportFileName(format));
|
|
1865
|
+
try {
|
|
1866
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
1867
|
+
await writeFile2(filePath, content, "utf8");
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
throw new CliError(`Could not write export file ${filePath}.`, 1, {
|
|
1870
|
+
cause: error instanceof Error ? error : void 0
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
958
1873
|
if (options.quiet) {
|
|
959
|
-
writeStdout(
|
|
1874
|
+
writeStdout(filePath);
|
|
960
1875
|
return;
|
|
961
1876
|
}
|
|
962
1877
|
writeStdout(response.message);
|
|
963
|
-
writeStdout(
|
|
1878
|
+
writeStdout(
|
|
1879
|
+
`Saved ${links.length} link${links.length === 1 ? "" : "s"} to ${filePath}.`
|
|
1880
|
+
);
|
|
964
1881
|
}
|
|
965
|
-
|
|
966
|
-
// src/commands/list.ts
|
|
967
|
-
async function listCommand(options) {
|
|
1882
|
+
async function listLinks(options) {
|
|
968
1883
|
const config = await getAuthConfig(process.env);
|
|
969
1884
|
const response = await new ApiClient(config).listUrls({
|
|
970
1885
|
page: options.page,
|
|
@@ -991,18 +1906,52 @@ async function listCommand(options) {
|
|
|
991
1906
|
writeStdout(formatLinksTable(links));
|
|
992
1907
|
writeStdout(formatListSummary(response.data, links.length));
|
|
993
1908
|
}
|
|
1909
|
+
async function getLink(idOrAlias, options) {
|
|
1910
|
+
const config = await getAuthConfig(process.env);
|
|
1911
|
+
const response = await new ApiClient(config).getUrl(idOrAlias);
|
|
1912
|
+
if (options.json) {
|
|
1913
|
+
writeJson(response);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
if (options.quiet) {
|
|
1917
|
+
writeStdout(getQuietLinkValue(response.data));
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
writeStdout(response.message);
|
|
1921
|
+
writeStdout(formatLinkDetails(response.data));
|
|
1922
|
+
}
|
|
1923
|
+
async function deleteLink(idOrAlias, options) {
|
|
1924
|
+
const config = await getAuthConfig(process.env);
|
|
1925
|
+
const client = new ApiClient(config);
|
|
1926
|
+
const lookupResponse = await client.getUrl(idOrAlias);
|
|
1927
|
+
const resolvedId = getLinkId(lookupResponse.data);
|
|
1928
|
+
if (!resolvedId) {
|
|
1929
|
+
throw new CliError(
|
|
1930
|
+
"PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
const response = await client.deleteUrl(resolvedId);
|
|
1934
|
+
if (options.json) {
|
|
1935
|
+
writeJson(response);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (options.quiet) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
writeStdout(response.message);
|
|
1942
|
+
}
|
|
994
1943
|
|
|
995
1944
|
// src/commands/login.ts
|
|
996
|
-
async function
|
|
1945
|
+
async function login(options) {
|
|
997
1946
|
const credentials = getLoginConfig(options, process.env);
|
|
998
1947
|
const client = new ApiClient(credentials);
|
|
999
1948
|
const response = await client.whoami();
|
|
1000
1949
|
await new ConfigStore().save(credentials);
|
|
1001
1950
|
const responseBody = {
|
|
1002
1951
|
success: true,
|
|
1003
|
-
message: `Saved credentials for ${credentials.
|
|
1952
|
+
message: `Saved credentials for ${credentials.apiBaseUrl}.`,
|
|
1004
1953
|
data: {
|
|
1005
|
-
|
|
1954
|
+
apiBaseUrl: credentials.apiBaseUrl,
|
|
1006
1955
|
user: response.data
|
|
1007
1956
|
},
|
|
1008
1957
|
timestamp: response.timestamp
|
|
@@ -1014,7 +1963,7 @@ async function loginCommand(options) {
|
|
|
1014
1963
|
if (options.quiet) {
|
|
1015
1964
|
return;
|
|
1016
1965
|
}
|
|
1017
|
-
writeStdout(`Saved credentials for ${credentials.
|
|
1966
|
+
writeStdout(`Saved credentials for ${credentials.apiBaseUrl}`);
|
|
1018
1967
|
writeStdout(`Authenticated as ${userLabel(response.data)}`);
|
|
1019
1968
|
writeStdout(userTable(response.data));
|
|
1020
1969
|
}
|
|
@@ -1023,18 +1972,18 @@ async function loginCommand(options) {
|
|
|
1023
1972
|
function hasEnvConfig(env) {
|
|
1024
1973
|
return Boolean(env.PEAKURL_BASE_URL?.trim() || env.PEAKURL_API_KEY?.trim());
|
|
1025
1974
|
}
|
|
1026
|
-
async function
|
|
1975
|
+
async function logout(options) {
|
|
1027
1976
|
const store = new ConfigStore();
|
|
1028
1977
|
const saved = await store.load();
|
|
1029
1978
|
const removed = await store.clear();
|
|
1030
1979
|
const envConfig = hasEnvConfig(process.env);
|
|
1031
|
-
const message = removed && saved?.
|
|
1980
|
+
const message = removed && saved?.apiBaseUrl ? `Logged out. Removed saved credentials for ${saved.apiBaseUrl}.` : removed ? "Logged out. Removed saved PeakURL credentials." : "Already logged out. No saved PeakURL credentials were found.";
|
|
1032
1981
|
const responseBody = {
|
|
1033
1982
|
success: true,
|
|
1034
1983
|
message,
|
|
1035
1984
|
data: {
|
|
1036
1985
|
removed,
|
|
1037
|
-
|
|
1986
|
+
apiBaseUrl: saved?.apiBaseUrl,
|
|
1038
1987
|
envCredentialsActive: envConfig
|
|
1039
1988
|
},
|
|
1040
1989
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1054,19 +2003,35 @@ async function logoutCommand(options) {
|
|
|
1054
2003
|
}
|
|
1055
2004
|
}
|
|
1056
2005
|
|
|
2006
|
+
// src/commands/status.ts
|
|
2007
|
+
async function status(options) {
|
|
2008
|
+
const config = await getAuthConfig(process.env);
|
|
2009
|
+
const response = await new ApiClient(config).getStatus();
|
|
2010
|
+
if (options.json) {
|
|
2011
|
+
writeJson(response);
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
if (options.quiet) {
|
|
2015
|
+
writeStdout(getStatusValue(response.data));
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
writeStdout(response.message);
|
|
2019
|
+
writeStdout(formatStatusReport(response.data));
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1057
2022
|
// src/commands/update.ts
|
|
1058
|
-
async function
|
|
1059
|
-
const
|
|
2023
|
+
async function checkUpdate(options, currentVersion) {
|
|
2024
|
+
const status2 = await getUpdateStatus(currentVersion, process.env, {
|
|
1060
2025
|
forceRefresh: true
|
|
1061
2026
|
});
|
|
1062
2027
|
const responseBody = {
|
|
1063
2028
|
success: true,
|
|
1064
|
-
message:
|
|
2029
|
+
message: status2.isOutdated ? `A newer PeakURL CLI version is available (${status2.latestVersion}).` : `PeakURL CLI ${status2.currentVersion} is up to date.`,
|
|
1065
2030
|
data: {
|
|
1066
|
-
currentVersion:
|
|
1067
|
-
latestVersion:
|
|
1068
|
-
isOutdated:
|
|
1069
|
-
installCommand:
|
|
2031
|
+
currentVersion: status2.currentVersion,
|
|
2032
|
+
latestVersion: status2.latestVersion,
|
|
2033
|
+
isOutdated: status2.isOutdated,
|
|
2034
|
+
installCommand: status2.installCommand,
|
|
1070
2035
|
checkOnly: Boolean(options.check)
|
|
1071
2036
|
},
|
|
1072
2037
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1075,28 +2040,108 @@ async function updateCommand(options, currentVersion) {
|
|
|
1075
2040
|
writeJson(responseBody);
|
|
1076
2041
|
return;
|
|
1077
2042
|
}
|
|
1078
|
-
if (!
|
|
2043
|
+
if (!status2.isOutdated) {
|
|
1079
2044
|
if (!options.quiet) {
|
|
1080
|
-
writeStdout(`PeakURL CLI ${
|
|
2045
|
+
writeStdout(`PeakURL CLI ${status2.currentVersion} is up to date.`);
|
|
1081
2046
|
}
|
|
1082
2047
|
return;
|
|
1083
2048
|
}
|
|
1084
2049
|
if (options.quiet) {
|
|
1085
|
-
writeStdout(
|
|
2050
|
+
writeStdout(status2.installCommand);
|
|
1086
2051
|
return;
|
|
1087
2052
|
}
|
|
1088
2053
|
writeNoticeBox(
|
|
1089
2054
|
"Update Available",
|
|
1090
2055
|
[
|
|
1091
|
-
`peakurl ${
|
|
1092
|
-
`Run: ${
|
|
2056
|
+
`peakurl ${status2.currentVersion} -> ${status2.latestVersion}`,
|
|
2057
|
+
`Run: ${status2.installCommand}`
|
|
1093
2058
|
],
|
|
1094
2059
|
"stdout"
|
|
1095
2060
|
);
|
|
1096
2061
|
}
|
|
1097
2062
|
|
|
2063
|
+
// src/commands/webhooks.ts
|
|
2064
|
+
async function listWebhooks(options) {
|
|
2065
|
+
const config = await getAuthConfig(process.env);
|
|
2066
|
+
const response = await new ApiClient(config).listWebhooks();
|
|
2067
|
+
if (options.json) {
|
|
2068
|
+
writeJson(response);
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
if (options.quiet) {
|
|
2072
|
+
for (const webhook of response.data ?? []) {
|
|
2073
|
+
const value = getQuietWebhookValue(webhook);
|
|
2074
|
+
if (value) {
|
|
2075
|
+
writeStdout(value);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
writeStdout(response.message);
|
|
2081
|
+
writeStdout(formatWebhooksTable(response.data ?? []));
|
|
2082
|
+
writeStdout(formatWebhooksSummary(response.data ?? []));
|
|
2083
|
+
}
|
|
2084
|
+
async function createWebhook(url, options) {
|
|
2085
|
+
const events = Array.from(new Set(options.event ?? []));
|
|
2086
|
+
if (events.length === 0) {
|
|
2087
|
+
throw new CliError(
|
|
2088
|
+
"At least one webhook event is required. Use `--event <event>` and run `peakurl webhook events` to see the supported values."
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
const config = await getAuthConfig(process.env);
|
|
2092
|
+
const response = await new ApiClient(config).createWebhook({
|
|
2093
|
+
url: normalizeWebhookUrl(url),
|
|
2094
|
+
events
|
|
2095
|
+
});
|
|
2096
|
+
if (options.json) {
|
|
2097
|
+
writeJson(response);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (options.quiet) {
|
|
2101
|
+
writeStdout(getQuietWebhookValue(response.data));
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
writeStdout(response.message);
|
|
2105
|
+
writeStdout(formatWebhookDetails(response.data));
|
|
2106
|
+
if (response.data.secret) {
|
|
2107
|
+
writeStdout("Save the signing secret now. PeakURL only shows it once.");
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
async function deleteWebhook(id, options) {
|
|
2111
|
+
const config = await getAuthConfig(process.env);
|
|
2112
|
+
const response = await new ApiClient(config).deleteWebhook(id.trim());
|
|
2113
|
+
if (options.json) {
|
|
2114
|
+
writeJson(response);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (options.quiet) {
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
writeStdout(response.message);
|
|
2121
|
+
}
|
|
2122
|
+
async function listWebhookEvents(options) {
|
|
2123
|
+
const response = {
|
|
2124
|
+
success: true,
|
|
2125
|
+
message: "Webhook events loaded.",
|
|
2126
|
+
data: WEBHOOK_EVENTS,
|
|
2127
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2128
|
+
};
|
|
2129
|
+
if (options.json) {
|
|
2130
|
+
writeJson(response);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (options.quiet) {
|
|
2134
|
+
for (const event of WEBHOOK_EVENTS) {
|
|
2135
|
+
writeStdout(event.id);
|
|
2136
|
+
}
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
writeStdout(response.message);
|
|
2140
|
+
writeStdout(formatWebhookEventsTable());
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1098
2143
|
// src/commands/whoami.ts
|
|
1099
|
-
async function
|
|
2144
|
+
async function whoami(options) {
|
|
1100
2145
|
const config = await getAuthConfig(process.env);
|
|
1101
2146
|
const response = await new ApiClient(config).whoami();
|
|
1102
2147
|
if (options.json) {
|
|
@@ -1125,10 +2170,23 @@ function parseNumber(label) {
|
|
|
1125
2170
|
}
|
|
1126
2171
|
async function getCliVersion() {
|
|
1127
2172
|
const packageJson = new URL("../package.json", import.meta.url);
|
|
1128
|
-
const content = await
|
|
2173
|
+
const content = await readFile3(packageJson, "utf8");
|
|
1129
2174
|
const parsed = JSON.parse(content);
|
|
1130
2175
|
return parsed.version || "0.0.0";
|
|
1131
2176
|
}
|
|
2177
|
+
function getRetryCommandName(argv) {
|
|
2178
|
+
const first = argv[2]?.trim();
|
|
2179
|
+
if (!first || first.startsWith("-")) {
|
|
2180
|
+
return void 0;
|
|
2181
|
+
}
|
|
2182
|
+
if (first === "webhook" || first === "webhooks") {
|
|
2183
|
+
const second = argv[3]?.trim();
|
|
2184
|
+
if (second && !second.startsWith("-")) {
|
|
2185
|
+
return `${first} ${second}`;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return first;
|
|
2189
|
+
}
|
|
1132
2190
|
async function main() {
|
|
1133
2191
|
const program = new Command();
|
|
1134
2192
|
const version = await getCliVersion();
|
|
@@ -1136,11 +2194,16 @@ async function main() {
|
|
|
1136
2194
|
"after",
|
|
1137
2195
|
`
|
|
1138
2196
|
Examples:
|
|
1139
|
-
peakurl login --base-url https://
|
|
2197
|
+
peakurl login --base-url https://example.com/api/v1 --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
1140
2198
|
peakurl whoami --json
|
|
1141
2199
|
peakurl logout
|
|
2200
|
+
peakurl status
|
|
1142
2201
|
peakurl create https://example.com --alias example
|
|
2202
|
+
peakurl import ./links.csv
|
|
2203
|
+
peakurl export --format csv
|
|
1143
2204
|
peakurl list --limit 10
|
|
2205
|
+
peakurl webhook list
|
|
2206
|
+
peakurl webhook create https://example.com/api/webhooks/peakurl --event link.clicked
|
|
1144
2207
|
peakurl update --check
|
|
1145
2208
|
peakurl get example
|
|
1146
2209
|
peakurl delete example --quiet`
|
|
@@ -1158,23 +2221,39 @@ Examples:
|
|
|
1158
2221
|
"Save PeakURL credentials after verifying them with GET /users/me."
|
|
1159
2222
|
).option(
|
|
1160
2223
|
"--base-url <url>",
|
|
1161
|
-
"PeakURL base URL, for example https://
|
|
1162
|
-
).option("--api-key <token>", "PeakURL API key to store").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(
|
|
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(
|
|
1164
|
-
program.command("logout").description("Remove saved PeakURL credentials from this device.").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(
|
|
2224
|
+
"PeakURL API base URL, for example https://example.com/api/v1"
|
|
2225
|
+
).option("--api-key <token>", "PeakURL API key to store").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(login);
|
|
2226
|
+
program.command("whoami").description("Show the current authenticated PeakURL user.").option("--json", "Print machine-readable output").option("--quiet", "Print a minimal identity value").action(whoami);
|
|
2227
|
+
program.command("logout").description("Remove saved PeakURL credentials from this device.").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(logout);
|
|
2228
|
+
program.command("status").description("Show the current PeakURL system status snapshot.").option("--json", "Print machine-readable output").option("--quiet", "Print only the overall health value").action(status);
|
|
1165
2229
|
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(
|
|
1166
2230
|
"--status <status>",
|
|
1167
2231
|
"Link status, for example active or paused"
|
|
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(
|
|
1169
|
-
program.command("
|
|
1170
|
-
|
|
1171
|
-
|
|
2232
|
+
).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(createLink);
|
|
2233
|
+
program.command("import").description(
|
|
2234
|
+
"Import multiple short links from a local CSV, JSON, or XML file."
|
|
2235
|
+
).argument("<file>", "Path to the import file").option("--format <format>", "File format: csv, json, or xml").option("--json", "Print machine-readable output").option("--quiet", "Print only the created short URLs").action(importLinks);
|
|
2236
|
+
program.command("export").description(
|
|
2237
|
+
"Export accessible links as a local CSV, JSON, or XML file."
|
|
2238
|
+
).option("--format <format>", "File format: csv, json, or xml").option("--output <path>", "Write the export to a specific file").option("--stdout", "Write the raw export content to stdout").option("--search <query>", "Search term").option("--sort-by <field>", "Sort field").option("--sort-order <order>", "Sort order, for example asc or desc").option("--quiet", "Print only the saved export path").action(exportLinks);
|
|
2239
|
+
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(listLinks);
|
|
2240
|
+
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(getLink);
|
|
2241
|
+
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(deleteLink);
|
|
1172
2242
|
program.command("update").description(
|
|
1173
2243
|
"Check for a newer CLI version and print the npm command to install it."
|
|
1174
2244
|
).option(
|
|
1175
2245
|
"--check",
|
|
1176
2246
|
"Alias for checking update status without changing anything"
|
|
1177
|
-
).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action((options) =>
|
|
2247
|
+
).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action((options) => checkUpdate(options, version));
|
|
2248
|
+
const webhook = program.command("webhook").alias("webhooks").description("Manage outbound webhook integrations.");
|
|
2249
|
+
webhook.command("list").description("List outbound webhooks.").option("--json", "Print machine-readable output").option("--quiet", "Print minimal webhook identifiers").action(listWebhooks);
|
|
2250
|
+
webhook.command("create").description("Create an outbound webhook.").argument("<url>", "Webhook endpoint URL").option(
|
|
2251
|
+
"--event <event>",
|
|
2252
|
+
"Webhook event id, for example link.clicked",
|
|
2253
|
+
parseWebhookEvents
|
|
2254
|
+
).option("--json", "Print machine-readable output").option("--quiet", "Print only the created webhook ID").action(createWebhook);
|
|
2255
|
+
webhook.command("delete").description("Delete an outbound webhook by id.").argument("<id>", "Webhook identifier").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteWebhook);
|
|
2256
|
+
webhook.command("events").description("List the webhook events supported by the CLI.").option("--json", "Print machine-readable output").option("--quiet", "Print only event ids").action(listWebhookEvents);
|
|
1178
2257
|
try {
|
|
1179
2258
|
await program.parseAsync(process.argv);
|
|
1180
2259
|
} catch (error) {
|
|
@@ -1183,8 +2262,19 @@ Examples:
|
|
|
1183
2262
|
}
|
|
1184
2263
|
const cliError = ensureCliError(error);
|
|
1185
2264
|
if (cliError.kind === "auth_required") {
|
|
1186
|
-
|
|
1187
|
-
writeStderr(
|
|
2265
|
+
const commandName = getRetryCommandName(process.argv);
|
|
2266
|
+
writeStderr("Authentication required.");
|
|
2267
|
+
writeStderr("PeakURL could not find credentials for this command.");
|
|
2268
|
+
writeStderr(
|
|
2269
|
+
"Use one of the first two steps below, then run the last command."
|
|
2270
|
+
);
|
|
2271
|
+
writeStderr(
|
|
2272
|
+
formatTable(
|
|
2273
|
+
["Step", "Command", "Notes"],
|
|
2274
|
+
authRows(commandName),
|
|
2275
|
+
"stderr"
|
|
2276
|
+
)
|
|
2277
|
+
);
|
|
1188
2278
|
} else {
|
|
1189
2279
|
writeStderr(cliError.message);
|
|
1190
2280
|
}
|