peakurl 0.1.2 → 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 +108 -12
- package/bin/peakurl.js +1420 -260
- package/package.json +1 -1
package/bin/peakurl.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
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;
|
|
14
|
+
kind;
|
|
10
15
|
constructor(message, exitCode = 1, options) {
|
|
11
16
|
super(message, options);
|
|
12
17
|
this.name = "CliError";
|
|
13
18
|
this.exitCode = exitCode;
|
|
19
|
+
this.kind = options?.kind;
|
|
14
20
|
}
|
|
15
21
|
};
|
|
16
|
-
function
|
|
22
|
+
function ensureCliError(error) {
|
|
17
23
|
if (error instanceof CliError) {
|
|
18
24
|
return error;
|
|
19
25
|
}
|
|
@@ -32,28 +38,30 @@ function validateHttpUrl(parsed, label) {
|
|
|
32
38
|
throw new CliError(`${label} must not include embedded credentials.`);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
|
-
function
|
|
41
|
+
function getApiBaseUrl(value) {
|
|
36
42
|
const input = value.trim();
|
|
37
43
|
if (!input) {
|
|
38
|
-
throw new CliError("A PeakURL base URL is required.");
|
|
44
|
+
throw new CliError("A PeakURL API base URL is required.");
|
|
39
45
|
}
|
|
40
46
|
let parsed;
|
|
41
47
|
try {
|
|
42
48
|
parsed = new URL(input);
|
|
43
49
|
} catch {
|
|
44
|
-
throw new CliError(`Invalid base URL: ${value}`);
|
|
50
|
+
throw new CliError(`Invalid API base URL: ${value}`);
|
|
45
51
|
}
|
|
46
|
-
validateHttpUrl(parsed, "PeakURL base URL");
|
|
52
|
+
validateHttpUrl(parsed, "PeakURL API base URL");
|
|
47
53
|
parsed.hash = "";
|
|
48
54
|
parsed.search = "";
|
|
49
55
|
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
50
|
-
|
|
51
|
-
|
|
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}`;
|
|
52
60
|
}
|
|
53
|
-
function buildApiUrl(
|
|
54
|
-
const cleanBaseUrl =
|
|
61
|
+
function buildApiUrl(apiBaseUrl, path, query) {
|
|
62
|
+
const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
|
|
55
63
|
const cleanPath = path.replace(/^\/+/, "");
|
|
56
|
-
const url = new URL(
|
|
64
|
+
const url = new URL(cleanPath, `${cleanBaseUrl}/`);
|
|
57
65
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
58
66
|
if (value === void 0 || value === "") {
|
|
59
67
|
continue;
|
|
@@ -78,24 +86,40 @@ function normalizeDestinationUrl(value) {
|
|
|
78
86
|
throw new CliError(`Invalid destination URL: ${value}`);
|
|
79
87
|
}
|
|
80
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
|
+
}
|
|
81
105
|
|
|
82
106
|
// src/api/client.ts
|
|
83
|
-
function
|
|
107
|
+
function isApiResponse(value) {
|
|
84
108
|
return Boolean(
|
|
85
109
|
value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
|
|
86
110
|
);
|
|
87
111
|
}
|
|
88
|
-
function
|
|
112
|
+
function networkError(apiBaseUrl, error) {
|
|
89
113
|
if (error instanceof Error && error.message) {
|
|
90
|
-
return `Could not reach PeakURL at ${
|
|
114
|
+
return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
|
|
91
115
|
}
|
|
92
|
-
return `Could not reach PeakURL at ${
|
|
116
|
+
return `Could not reach PeakURL at ${apiBaseUrl}.`;
|
|
93
117
|
}
|
|
94
|
-
var
|
|
118
|
+
var ApiClient = class {
|
|
95
119
|
/**
|
|
96
120
|
* Creates a client bound to one resolved credential set.
|
|
97
121
|
*
|
|
98
|
-
* @param config
|
|
122
|
+
* @param config Explicit API base URL plus bearer API key.
|
|
99
123
|
*/
|
|
100
124
|
constructor(config) {
|
|
101
125
|
this.config = config;
|
|
@@ -112,6 +136,14 @@ var PeakUrlApiClient = class {
|
|
|
112
136
|
whoami() {
|
|
113
137
|
return this.request("GET", "users/me");
|
|
114
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
|
+
}
|
|
115
147
|
/**
|
|
116
148
|
* Creates a short URL.
|
|
117
149
|
*
|
|
@@ -131,13 +163,31 @@ var PeakUrlApiClient = class {
|
|
|
131
163
|
* @returns API response envelope containing list data.
|
|
132
164
|
*/
|
|
133
165
|
listUrls(query) {
|
|
166
|
+
return this.request("GET", "urls", void 0, query);
|
|
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) {
|
|
134
175
|
return this.request(
|
|
135
176
|
"GET",
|
|
136
|
-
"urls",
|
|
177
|
+
"urls/export",
|
|
137
178
|
void 0,
|
|
138
179
|
query
|
|
139
180
|
);
|
|
140
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
|
+
}
|
|
141
191
|
/**
|
|
142
192
|
* Loads a single short URL by identifier or alias.
|
|
143
193
|
*
|
|
@@ -167,6 +217,35 @@ var PeakUrlApiClient = class {
|
|
|
167
217
|
`urls/${encodeURIComponent(id)}`
|
|
168
218
|
);
|
|
169
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
|
+
}
|
|
170
249
|
/**
|
|
171
250
|
* Performs one authenticated API request and normalizes the response.
|
|
172
251
|
*
|
|
@@ -178,7 +257,7 @@ var PeakUrlApiClient = class {
|
|
|
178
257
|
* @throws {CliError} When the network request fails or the API returns an error.
|
|
179
258
|
*/
|
|
180
259
|
async request(method, path, body, query) {
|
|
181
|
-
const url = buildApiUrl(this.config.
|
|
260
|
+
const url = buildApiUrl(this.config.apiBaseUrl, path, query);
|
|
182
261
|
let response;
|
|
183
262
|
try {
|
|
184
263
|
response = await fetch(url, {
|
|
@@ -191,13 +270,9 @@ var PeakUrlApiClient = class {
|
|
|
191
270
|
body: body ? JSON.stringify(body) : void 0
|
|
192
271
|
});
|
|
193
272
|
} catch (error) {
|
|
194
|
-
throw new CliError(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{
|
|
198
|
-
cause: error instanceof Error ? error : void 0
|
|
199
|
-
}
|
|
200
|
-
);
|
|
273
|
+
throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
|
|
274
|
+
cause: error instanceof Error ? error : void 0
|
|
275
|
+
});
|
|
201
276
|
}
|
|
202
277
|
const rawText = await response.text();
|
|
203
278
|
if (!rawText) {
|
|
@@ -224,7 +299,7 @@ var PeakUrlApiClient = class {
|
|
|
224
299
|
}
|
|
225
300
|
throw new CliError("PeakURL returned an invalid JSON response.");
|
|
226
301
|
}
|
|
227
|
-
if (!
|
|
302
|
+
if (!isApiResponse(parsed)) {
|
|
228
303
|
throw new CliError(
|
|
229
304
|
"PeakURL returned an unexpected response envelope."
|
|
230
305
|
);
|
|
@@ -241,18 +316,18 @@ var PeakUrlApiClient = class {
|
|
|
241
316
|
};
|
|
242
317
|
|
|
243
318
|
// src/config/store.ts
|
|
244
|
-
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
319
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
245
320
|
import { dirname, join } from "path";
|
|
246
321
|
import envPaths from "env-paths";
|
|
247
322
|
var CONFIG_FILENAME = "config.json";
|
|
248
323
|
var STATE_FILENAME = "state.json";
|
|
249
|
-
function
|
|
324
|
+
function getConfigPath() {
|
|
250
325
|
return join(envPaths("peakurl", { suffix: "" }).config, CONFIG_FILENAME);
|
|
251
326
|
}
|
|
252
|
-
function
|
|
327
|
+
function getStatePath() {
|
|
253
328
|
return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
|
|
254
329
|
}
|
|
255
|
-
async function
|
|
330
|
+
async function ensureParentDir(filePath) {
|
|
256
331
|
const directory = dirname(filePath);
|
|
257
332
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
258
333
|
return directory;
|
|
@@ -264,7 +339,7 @@ var ConfigStore = class {
|
|
|
264
339
|
*
|
|
265
340
|
* @param filePath Optional override used by tests or advanced callers.
|
|
266
341
|
*/
|
|
267
|
-
constructor(filePath =
|
|
342
|
+
constructor(filePath = getConfigPath()) {
|
|
268
343
|
this.filePath = filePath;
|
|
269
344
|
}
|
|
270
345
|
/**
|
|
@@ -280,11 +355,12 @@ var ConfigStore = class {
|
|
|
280
355
|
try {
|
|
281
356
|
const content = await readFile(this.filePath, "utf8");
|
|
282
357
|
const parsed = JSON.parse(content);
|
|
283
|
-
|
|
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") {
|
|
284
360
|
throw new CliError(`Invalid config file: ${this.filePath}`);
|
|
285
361
|
}
|
|
286
362
|
return {
|
|
287
|
-
|
|
363
|
+
apiBaseUrl,
|
|
288
364
|
apiKey: parsed.apiKey
|
|
289
365
|
};
|
|
290
366
|
} catch (error) {
|
|
@@ -312,7 +388,7 @@ var ConfigStore = class {
|
|
|
312
388
|
* @param config Normalized credential set to write.
|
|
313
389
|
*/
|
|
314
390
|
async save(config) {
|
|
315
|
-
const directory = await
|
|
391
|
+
const directory = await ensureParentDir(this.filePath);
|
|
316
392
|
await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
|
|
317
393
|
`, {
|
|
318
394
|
mode: 384
|
|
@@ -323,6 +399,29 @@ var ConfigStore = class {
|
|
|
323
399
|
} catch {
|
|
324
400
|
}
|
|
325
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Removes the stored credential file.
|
|
404
|
+
*
|
|
405
|
+
* @returns `true` when a saved config file existed and was removed.
|
|
406
|
+
* @throws {CliError} When the file exists but cannot be removed.
|
|
407
|
+
*/
|
|
408
|
+
async clear() {
|
|
409
|
+
try {
|
|
410
|
+
await unlink(this.filePath);
|
|
411
|
+
return true;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code === "ENOENT") {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
throw new CliError(
|
|
417
|
+
`Could not remove PeakURL config at ${this.filePath}.`,
|
|
418
|
+
1,
|
|
419
|
+
{
|
|
420
|
+
cause: error instanceof Error ? error : void 0
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
326
425
|
};
|
|
327
426
|
var StateStore = class {
|
|
328
427
|
filePath;
|
|
@@ -331,7 +430,7 @@ var StateStore = class {
|
|
|
331
430
|
*
|
|
332
431
|
* @param filePath Optional override used by tests or advanced callers.
|
|
333
432
|
*/
|
|
334
|
-
constructor(filePath =
|
|
433
|
+
constructor(filePath = getStatePath()) {
|
|
335
434
|
this.filePath = filePath;
|
|
336
435
|
}
|
|
337
436
|
/**
|
|
@@ -357,7 +456,7 @@ var StateStore = class {
|
|
|
357
456
|
* @param state State payload to save.
|
|
358
457
|
*/
|
|
359
458
|
async save(state) {
|
|
360
|
-
const directory = await
|
|
459
|
+
const directory = await ensureParentDir(this.filePath);
|
|
361
460
|
await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
|
|
362
461
|
`, {
|
|
363
462
|
mode: 384
|
|
@@ -371,34 +470,159 @@ var StateStore = class {
|
|
|
371
470
|
};
|
|
372
471
|
|
|
373
472
|
// src/lib/auth.ts
|
|
374
|
-
|
|
375
|
-
|
|
473
|
+
var AUTH_REQUIRED_MESSAGE = "PeakURL credentials are not configured.";
|
|
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"]
|
|
492
|
+
];
|
|
493
|
+
return rows2;
|
|
494
|
+
}
|
|
495
|
+
function getLoginConfig(input, env) {
|
|
496
|
+
const apiBaseUrl = input.baseUrl?.trim() || env.PEAKURL_BASE_URL?.trim();
|
|
376
497
|
const apiKey = input.apiKey?.trim() || env.PEAKURL_API_KEY?.trim();
|
|
377
|
-
if (!
|
|
498
|
+
if (!apiBaseUrl || !apiKey) {
|
|
378
499
|
throw new CliError(
|
|
379
500
|
"Missing credentials. Provide --base-url and --api-key, or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
|
|
380
501
|
);
|
|
381
502
|
}
|
|
382
503
|
return {
|
|
383
|
-
|
|
504
|
+
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
384
505
|
apiKey
|
|
385
506
|
};
|
|
386
507
|
}
|
|
387
|
-
async function
|
|
508
|
+
async function getAuthConfig(env, store = new ConfigStore()) {
|
|
388
509
|
const saved = await store.load();
|
|
389
|
-
const
|
|
510
|
+
const apiBaseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.apiBaseUrl;
|
|
390
511
|
const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
|
|
391
|
-
if (!
|
|
392
|
-
throw new CliError(
|
|
393
|
-
|
|
394
|
-
);
|
|
512
|
+
if (!apiBaseUrl || !apiKey) {
|
|
513
|
+
throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
|
|
514
|
+
kind: "auth_required"
|
|
515
|
+
});
|
|
395
516
|
}
|
|
396
517
|
return {
|
|
397
|
-
|
|
518
|
+
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
398
519
|
apiKey
|
|
399
520
|
};
|
|
400
521
|
}
|
|
401
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
|
+
|
|
402
626
|
// src/lib/output.ts
|
|
403
627
|
function writeStdout(message = "") {
|
|
404
628
|
process.stdout.write(`${message}
|
|
@@ -449,7 +673,7 @@ function writeNoticeBox(title, lines, target = "stderr") {
|
|
|
449
673
|
}
|
|
450
674
|
writeLine(bottomBorder);
|
|
451
675
|
}
|
|
452
|
-
function formatTable(headers,
|
|
676
|
+
function formatTable(headers, rows2, target = "stdout") {
|
|
453
677
|
const stream = target === "stdout" ? process.stdout : process.stderr;
|
|
454
678
|
const useTuiBox = stream.isTTY;
|
|
455
679
|
const border = useTuiBox ? {
|
|
@@ -477,14 +701,26 @@ function formatTable(headers, rows, target = "stdout") {
|
|
|
477
701
|
middleJunction: "+",
|
|
478
702
|
bottomJunction: "+"
|
|
479
703
|
};
|
|
704
|
+
const getLines = (value) => (value ?? "").split("\n");
|
|
480
705
|
const widths = headers.map(
|
|
481
706
|
(header, index) => Math.max(
|
|
482
707
|
header.length,
|
|
483
|
-
...
|
|
708
|
+
...rows2.flatMap(
|
|
709
|
+
(row2) => getLines(row2[index]).map((line) => line.length)
|
|
710
|
+
)
|
|
484
711
|
)
|
|
485
712
|
);
|
|
486
713
|
const formatTableBorder = (left, join2, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join2)}${right}`;
|
|
487
|
-
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
|
+
};
|
|
488
724
|
return [
|
|
489
725
|
formatTableBorder(border.topLeft, border.topJunction, border.topRight),
|
|
490
726
|
formatTableRow(headers),
|
|
@@ -493,7 +729,7 @@ function formatTable(headers, rows, target = "stdout") {
|
|
|
493
729
|
border.middleJunction,
|
|
494
730
|
border.separatorRight
|
|
495
731
|
),
|
|
496
|
-
...
|
|
732
|
+
...rows2.map(formatTableRow),
|
|
497
733
|
formatTableBorder(
|
|
498
734
|
border.bottomLeft,
|
|
499
735
|
border.bottomJunction,
|
|
@@ -505,20 +741,275 @@ function writeJson(value) {
|
|
|
505
741
|
writeStdout(JSON.stringify(value, null, 2));
|
|
506
742
|
}
|
|
507
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
|
+
|
|
508
999
|
// src/lib/links.ts
|
|
509
1000
|
var LIST_KEYS = ["urls", "items", "results"];
|
|
510
|
-
function
|
|
1001
|
+
function asObject(value) {
|
|
511
1002
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
512
1003
|
}
|
|
513
|
-
function
|
|
1004
|
+
function asString(value) {
|
|
514
1005
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
515
1006
|
}
|
|
516
|
-
function
|
|
1007
|
+
function asNumber(value) {
|
|
517
1008
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
518
1009
|
}
|
|
519
|
-
function
|
|
1010
|
+
function pickText(link, keys) {
|
|
520
1011
|
for (const key of keys) {
|
|
521
|
-
const value =
|
|
1012
|
+
const value = asString(link[key]);
|
|
522
1013
|
if (value) {
|
|
523
1014
|
return value;
|
|
524
1015
|
}
|
|
@@ -528,32 +1019,32 @@ function pickString(link, keys) {
|
|
|
528
1019
|
function truncate(value, maxLength) {
|
|
529
1020
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
530
1021
|
}
|
|
531
|
-
function
|
|
532
|
-
const record =
|
|
1022
|
+
function getListMeta(data) {
|
|
1023
|
+
const record = asObject(data);
|
|
533
1024
|
if (!record) {
|
|
534
1025
|
return null;
|
|
535
1026
|
}
|
|
536
|
-
const meta =
|
|
1027
|
+
const meta = asObject(record.meta);
|
|
537
1028
|
if (meta) {
|
|
538
1029
|
return {
|
|
539
|
-
page:
|
|
540
|
-
limit:
|
|
541
|
-
totalItems:
|
|
542
|
-
totalPages:
|
|
1030
|
+
page: asNumber(meta.page),
|
|
1031
|
+
limit: asNumber(meta.limit),
|
|
1032
|
+
totalItems: asNumber(meta.totalItems),
|
|
1033
|
+
totalPages: asNumber(meta.totalPages)
|
|
543
1034
|
};
|
|
544
1035
|
}
|
|
545
1036
|
return {
|
|
546
|
-
page:
|
|
547
|
-
limit:
|
|
548
|
-
totalItems:
|
|
549
|
-
totalPages:
|
|
1037
|
+
page: asNumber(record.page),
|
|
1038
|
+
limit: asNumber(record.limit),
|
|
1039
|
+
totalItems: asNumber(record.total),
|
|
1040
|
+
totalPages: asNumber(record.totalPages)
|
|
550
1041
|
};
|
|
551
1042
|
}
|
|
552
1043
|
function extractLinks(data) {
|
|
553
1044
|
if (Array.isArray(data)) {
|
|
554
1045
|
return data;
|
|
555
1046
|
}
|
|
556
|
-
const record =
|
|
1047
|
+
const record = asObject(data);
|
|
557
1048
|
if (record) {
|
|
558
1049
|
for (const key of LIST_KEYS) {
|
|
559
1050
|
const value = record[key];
|
|
@@ -565,16 +1056,16 @@ function extractLinks(data) {
|
|
|
565
1056
|
return [];
|
|
566
1057
|
}
|
|
567
1058
|
function getLinkId(link) {
|
|
568
|
-
return
|
|
1059
|
+
return pickText(link, ["id", "_id", "urlId"]) || (link.id !== void 0 ? String(link.id) : void 0);
|
|
569
1060
|
}
|
|
570
1061
|
function getLinkAlias(link) {
|
|
571
|
-
return
|
|
1062
|
+
return pickText(link, ["alias", "shortCode", "slug", "code"]);
|
|
572
1063
|
}
|
|
573
1064
|
function getLinkShortUrl(link) {
|
|
574
|
-
return
|
|
1065
|
+
return pickText(link, ["shortUrl", "shortLink", "shortURL", "url"]);
|
|
575
1066
|
}
|
|
576
1067
|
function getLinkDestination(link) {
|
|
577
|
-
return
|
|
1068
|
+
return pickText(link, [
|
|
578
1069
|
"destinationUrl",
|
|
579
1070
|
"originalUrl",
|
|
580
1071
|
"targetUrl",
|
|
@@ -590,14 +1081,14 @@ function formatLinkDetails(link) {
|
|
|
590
1081
|
["Alias", getLinkAlias(link)],
|
|
591
1082
|
["Short URL", getLinkShortUrl(link)],
|
|
592
1083
|
["Destination", getLinkDestination(link)],
|
|
593
|
-
["Title",
|
|
594
|
-
["Status",
|
|
1084
|
+
["Title", asString(link.title)],
|
|
1085
|
+
["Status", asString(link.status)],
|
|
595
1086
|
[
|
|
596
1087
|
"Clicks",
|
|
597
|
-
|
|
1088
|
+
asNumber(link.clicks) === void 0 ? void 0 : String(link.clicks)
|
|
598
1089
|
],
|
|
599
|
-
["Created",
|
|
600
|
-
["Updated",
|
|
1090
|
+
["Created", asString(link.createdAt)],
|
|
1091
|
+
["Updated", asString(link.updatedAt)]
|
|
601
1092
|
].filter((entry) => Boolean(entry[1]));
|
|
602
1093
|
return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
603
1094
|
}
|
|
@@ -606,17 +1097,17 @@ function formatLinksTable(links) {
|
|
|
606
1097
|
return "No links found.";
|
|
607
1098
|
}
|
|
608
1099
|
const headers = ["ID", "Alias", "Short URL", "Destination", "Status"];
|
|
609
|
-
const
|
|
1100
|
+
const rows2 = links.map((link) => [
|
|
610
1101
|
truncate(getLinkId(link) || "-", 18),
|
|
611
1102
|
truncate(getLinkAlias(link) || "-", 12),
|
|
612
1103
|
truncate(getLinkShortUrl(link) || "-", 36),
|
|
613
1104
|
truncate(getLinkDestination(link) || "-", 52),
|
|
614
|
-
truncate(
|
|
1105
|
+
truncate(asString(link.status) || "-", 12)
|
|
615
1106
|
]);
|
|
616
|
-
return formatTable(headers,
|
|
1107
|
+
return formatTable(headers, rows2);
|
|
617
1108
|
}
|
|
618
1109
|
function formatListSummary(data, count) {
|
|
619
|
-
const meta =
|
|
1110
|
+
const meta = getListMeta(data);
|
|
620
1111
|
if (!meta) {
|
|
621
1112
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
622
1113
|
}
|
|
@@ -629,157 +1120,321 @@ function formatListSummary(data, count) {
|
|
|
629
1120
|
return `${count} link${count === 1 ? "" : "s"} returned.`;
|
|
630
1121
|
}
|
|
631
1122
|
|
|
632
|
-
// src/
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
return void 0;
|
|
636
|
-
}
|
|
637
|
-
if (Number.isNaN(Date.parse(value))) {
|
|
638
|
-
throw new CliError(`Invalid expiration timestamp: ${value}`);
|
|
639
|
-
}
|
|
640
|
-
return value;
|
|
1123
|
+
// src/lib/status.ts
|
|
1124
|
+
function text3(value) {
|
|
1125
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
641
1126
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
destinationUrl: normalizeDestinationUrl(destinationUrl),
|
|
646
|
-
...options.alias ? { alias: options.alias } : {},
|
|
647
|
-
...options.title ? { title: options.title } : {},
|
|
648
|
-
...options.password ? { password: options.password } : {},
|
|
649
|
-
...options.status ? { status: options.status } : {},
|
|
650
|
-
...options.expiresAt ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
|
|
651
|
-
...options.utmSource ? { utmSource: options.utmSource } : {},
|
|
652
|
-
...options.utmMedium ? { utmMedium: options.utmMedium } : {},
|
|
653
|
-
...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
|
|
654
|
-
...options.utmTerm ? { utmTerm: options.utmTerm } : {},
|
|
655
|
-
...options.utmContent ? { utmContent: options.utmContent } : {}
|
|
656
|
-
});
|
|
657
|
-
if (options.json) {
|
|
658
|
-
writeJson(response);
|
|
659
|
-
return;
|
|
1127
|
+
function integer(value) {
|
|
1128
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1129
|
+
return value;
|
|
660
1130
|
}
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
return;
|
|
1131
|
+
if (typeof value === "string" && value.trim()) {
|
|
1132
|
+
const parsed = Number(value);
|
|
1133
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
664
1134
|
}
|
|
665
|
-
|
|
666
|
-
writeStdout(formatLinkDetails(response.data));
|
|
1135
|
+
return void 0;
|
|
667
1136
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const config = await resolveStoredConfig(process.env);
|
|
672
|
-
const client = new PeakUrlApiClient(config);
|
|
673
|
-
const lookupResponse = await client.getUrl(idOrAlias);
|
|
674
|
-
const resolvedId = getLinkId(lookupResponse.data);
|
|
675
|
-
if (!resolvedId) {
|
|
676
|
-
throw new CliError(
|
|
677
|
-
"PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
|
|
678
|
-
);
|
|
1137
|
+
function flag(value) {
|
|
1138
|
+
if (typeof value === "boolean") {
|
|
1139
|
+
return value;
|
|
679
1140
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
writeJson(response);
|
|
683
|
-
return;
|
|
1141
|
+
if (value === 1 || value === "1") {
|
|
1142
|
+
return true;
|
|
684
1143
|
}
|
|
685
|
-
if (
|
|
686
|
-
return;
|
|
1144
|
+
if (value === 0 || value === "0") {
|
|
1145
|
+
return false;
|
|
687
1146
|
}
|
|
688
|
-
|
|
1147
|
+
return void 0;
|
|
689
1148
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const response = await new PeakUrlApiClient(config).getUrl(idOrAlias);
|
|
695
|
-
if (options.json) {
|
|
696
|
-
writeJson(response);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (options.quiet) {
|
|
700
|
-
writeStdout(getQuietLinkValue(response.data));
|
|
701
|
-
return;
|
|
1149
|
+
function yesNo(value, yes = "Yes", no = "No") {
|
|
1150
|
+
const normalized = flag(value);
|
|
1151
|
+
if (normalized === void 0) {
|
|
1152
|
+
return void 0;
|
|
702
1153
|
}
|
|
703
|
-
|
|
704
|
-
writeStdout(formatLinkDetails(response.data));
|
|
1154
|
+
return normalized ? yes : no;
|
|
705
1155
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const response = await new PeakUrlApiClient(config).listUrls({
|
|
711
|
-
page: options.page,
|
|
712
|
-
limit: options.limit,
|
|
713
|
-
search: options.search,
|
|
714
|
-
sortBy: options.sortBy,
|
|
715
|
-
sortOrder: options.sortOrder
|
|
716
|
-
});
|
|
717
|
-
const links = extractLinks(response.data);
|
|
718
|
-
if (options.json) {
|
|
719
|
-
writeJson(response);
|
|
720
|
-
return;
|
|
1156
|
+
function formatState(value) {
|
|
1157
|
+
const normalized = text3(value)?.toLowerCase();
|
|
1158
|
+
if (!normalized) {
|
|
1159
|
+
return void 0;
|
|
721
1160
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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;
|
|
730
1170
|
}
|
|
731
|
-
writeStdout(response.message);
|
|
732
|
-
writeStdout(formatLinksTable(links));
|
|
733
|
-
writeStdout(formatListSummary(response.data, links.length));
|
|
734
1171
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1172
|
+
function formatCount(value) {
|
|
1173
|
+
const normalized = integer(value);
|
|
1174
|
+
return normalized === void 0 ? text3(value) : String(normalized);
|
|
739
1175
|
}
|
|
740
|
-
function
|
|
741
|
-
const
|
|
742
|
-
|
|
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]}`;
|
|
743
1190
|
}
|
|
744
|
-
function
|
|
745
|
-
|
|
1191
|
+
function formatSeconds(value) {
|
|
1192
|
+
const normalized = integer(value);
|
|
1193
|
+
return normalized === void 0 ? text3(value) : `${normalized} seconds`;
|
|
746
1194
|
}
|
|
747
|
-
function
|
|
748
|
-
const lines = [
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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");
|
|
756
1207
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|
772
1246
|
};
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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;
|
|
776
1405
|
}
|
|
777
|
-
|
|
778
|
-
|
|
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;
|
|
779
1416
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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.";
|
|
783
1438
|
}
|
|
784
1439
|
|
|
785
1440
|
// src/lib/update.ts
|
|
@@ -886,7 +1541,7 @@ function compareSemver(left, right) {
|
|
|
886
1541
|
function getUpdateInstallCommand() {
|
|
887
1542
|
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
888
1543
|
}
|
|
889
|
-
async function
|
|
1544
|
+
async function fetchLatestVersion(env) {
|
|
890
1545
|
const registryUrl = normalizeRegistryUrl(getRegistryBaseUrl(env));
|
|
891
1546
|
const url = `${registryUrl}/${PACKAGE_NAME}/latest`;
|
|
892
1547
|
const controller = new AbortController();
|
|
@@ -909,7 +1564,7 @@ async function fetchLatestPackageVersion(env) {
|
|
|
909
1564
|
clearTimeout(timeout);
|
|
910
1565
|
}
|
|
911
1566
|
}
|
|
912
|
-
async function
|
|
1567
|
+
async function getUpdateState(store) {
|
|
913
1568
|
const state = await store.load();
|
|
914
1569
|
return state.update ?? {};
|
|
915
1570
|
}
|
|
@@ -920,15 +1575,15 @@ async function saveUpdateState(store, update) {
|
|
|
920
1575
|
update
|
|
921
1576
|
});
|
|
922
1577
|
}
|
|
923
|
-
async function
|
|
1578
|
+
async function loadLatestVersion(env, options) {
|
|
924
1579
|
const store = options?.store ?? new StateStore();
|
|
925
|
-
const updateState = await
|
|
1580
|
+
const updateState = await getUpdateState(store);
|
|
926
1581
|
const lastCheckedAt = parseTime(updateState.lastCheckedAt);
|
|
927
1582
|
const now = Date.now();
|
|
928
1583
|
if (!options?.forceRefresh && updateState.latestVersion && lastCheckedAt !== null && now - lastCheckedAt < CACHE_TTL_MS) {
|
|
929
1584
|
return updateState.latestVersion;
|
|
930
1585
|
}
|
|
931
|
-
const latestVersion = await
|
|
1586
|
+
const latestVersion = await fetchLatestVersion(env);
|
|
932
1587
|
if (!latestVersion) {
|
|
933
1588
|
return updateState.latestVersion ?? null;
|
|
934
1589
|
}
|
|
@@ -940,7 +1595,7 @@ async function getLatestPackageVersion(env, options) {
|
|
|
940
1595
|
return latestVersion;
|
|
941
1596
|
}
|
|
942
1597
|
async function getUpdateStatus(currentVersion, env, options) {
|
|
943
|
-
const resolvedLatestVersion = await
|
|
1598
|
+
const resolvedLatestVersion = await loadLatestVersion(env, {
|
|
944
1599
|
forceRefresh: options?.forceRefresh,
|
|
945
1600
|
store: options?.store
|
|
946
1601
|
});
|
|
@@ -957,13 +1612,13 @@ async function getUpdateStatus(currentVersion, env, options) {
|
|
|
957
1612
|
installCommand: getUpdateInstallCommand()
|
|
958
1613
|
};
|
|
959
1614
|
}
|
|
960
|
-
function
|
|
1615
|
+
function showUpdateNotice(status2) {
|
|
961
1616
|
writeNoticeBox("Update Available", [
|
|
962
|
-
`${PACKAGE_NAME} ${
|
|
963
|
-
`Run: ${
|
|
1617
|
+
`${PACKAGE_NAME} ${status2.currentVersion} -> ${status2.latestVersion}`,
|
|
1618
|
+
`Run: ${status2.installCommand}`
|
|
964
1619
|
]);
|
|
965
1620
|
}
|
|
966
|
-
async function
|
|
1621
|
+
async function checkUpdates(options) {
|
|
967
1622
|
if (options.env.PEAKURL_DISABLE_UPDATE_CHECK === "1" || options.commandName === "update" || options.options?.json || options.options?.quiet) {
|
|
968
1623
|
return;
|
|
969
1624
|
}
|
|
@@ -971,39 +1626,412 @@ async function maybeShowUpdateNotice(options) {
|
|
|
971
1626
|
return;
|
|
972
1627
|
}
|
|
973
1628
|
const store = new StateStore();
|
|
974
|
-
const updateState = await
|
|
975
|
-
const
|
|
1629
|
+
const updateState = await getUpdateState(store);
|
|
1630
|
+
const status2 = await getUpdateStatus(options.currentVersion, options.env, {
|
|
976
1631
|
store
|
|
977
1632
|
});
|
|
978
|
-
if (!
|
|
1633
|
+
if (!status2.isOutdated) {
|
|
979
1634
|
return;
|
|
980
1635
|
}
|
|
981
1636
|
const lastNotifiedAt = parseTime(updateState.lastNotifiedAt);
|
|
982
|
-
const alreadyNotifiedForVersion = updateState.lastNotifiedVersion ===
|
|
1637
|
+
const alreadyNotifiedForVersion = updateState.lastNotifiedVersion === status2.latestVersion;
|
|
983
1638
|
if (alreadyNotifiedForVersion && lastNotifiedAt !== null && Date.now() - lastNotifiedAt < NOTICE_TTL_MS) {
|
|
984
1639
|
return;
|
|
985
1640
|
}
|
|
986
1641
|
await saveUpdateState(store, {
|
|
987
1642
|
...updateState,
|
|
988
1643
|
lastNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
989
|
-
lastNotifiedVersion:
|
|
1644
|
+
lastNotifiedVersion: status2.latestVersion
|
|
990
1645
|
});
|
|
991
|
-
|
|
1646
|
+
showUpdateNotice(status2);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/lib/users.ts
|
|
1650
|
+
function text4(value) {
|
|
1651
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1652
|
+
}
|
|
1653
|
+
function userLabel(user) {
|
|
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");
|
|
1656
|
+
}
|
|
1657
|
+
function userValue(user) {
|
|
1658
|
+
return text4(user.username) || text4(user.email) || String(user.id ?? "");
|
|
1659
|
+
}
|
|
1660
|
+
function userTable(user) {
|
|
1661
|
+
const rows2 = [
|
|
1662
|
+
["Name", userLabel(user)],
|
|
1663
|
+
["Username", text4(user.username)],
|
|
1664
|
+
["Email", text4(user.email)],
|
|
1665
|
+
["Role", text4(user.role)],
|
|
1666
|
+
["ID", user.id === void 0 ? void 0 : String(user.id)]
|
|
1667
|
+
].filter((entry) => Boolean(entry[1]));
|
|
1668
|
+
if (rows2.length === 0) {
|
|
1669
|
+
return "No user fields returned.";
|
|
1670
|
+
}
|
|
1671
|
+
return formatTable(["Field", "Value"], rows2);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
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
|
|
1781
|
+
function normalizeExpiresAt(value) {
|
|
1782
|
+
if (!value) {
|
|
1783
|
+
return void 0;
|
|
1784
|
+
}
|
|
1785
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
1786
|
+
throw new CliError(`Invalid expiration timestamp: ${value}`);
|
|
1787
|
+
}
|
|
1788
|
+
return value;
|
|
1789
|
+
}
|
|
1790
|
+
async function createLink(destinationUrl, options) {
|
|
1791
|
+
const config = await getAuthConfig(process.env);
|
|
1792
|
+
const response = await new ApiClient(config).createUrl({
|
|
1793
|
+
destinationUrl: normalizeDestinationUrl(destinationUrl),
|
|
1794
|
+
...options.alias ? { alias: options.alias } : {},
|
|
1795
|
+
...options.title ? { title: options.title } : {},
|
|
1796
|
+
...options.password ? { password: options.password } : {},
|
|
1797
|
+
...options.status ? { status: options.status } : {},
|
|
1798
|
+
...options.expiresAt ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
|
|
1799
|
+
...options.utmSource ? { utmSource: options.utmSource } : {},
|
|
1800
|
+
...options.utmMedium ? { utmMedium: options.utmMedium } : {},
|
|
1801
|
+
...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
|
|
1802
|
+
...options.utmTerm ? { utmTerm: options.utmTerm } : {},
|
|
1803
|
+
...options.utmContent ? { utmContent: options.utmContent } : {}
|
|
1804
|
+
});
|
|
1805
|
+
if (options.json) {
|
|
1806
|
+
writeJson(response);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (options.quiet) {
|
|
1810
|
+
writeStdout(getQuietLinkValue(response.data));
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
writeStdout(response.message);
|
|
1814
|
+
writeStdout(formatLinkDetails(response.data));
|
|
1815
|
+
}
|
|
1816
|
+
async function importLinks(filePath, options) {
|
|
1817
|
+
const format = getImportFormat(filePath, options.format);
|
|
1818
|
+
const urls = await readImportRows(filePath, format);
|
|
1819
|
+
const config = await getAuthConfig(process.env);
|
|
1820
|
+
const response = await new ApiClient(config).importUrls({ urls });
|
|
1821
|
+
const links = extractLinks(response.data?.results ?? []);
|
|
1822
|
+
const errors = response.data?.errors ?? [];
|
|
1823
|
+
if (options.json) {
|
|
1824
|
+
writeJson(response);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (options.quiet) {
|
|
1828
|
+
for (const link of links) {
|
|
1829
|
+
const value = getQuietLinkValue(link);
|
|
1830
|
+
if (value) {
|
|
1831
|
+
writeStdout(value);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
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 ?? {}));
|
|
1846
|
+
}
|
|
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";
|
|
1852
|
+
const config = await getAuthConfig(process.env);
|
|
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);
|
|
1862
|
+
return;
|
|
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
|
+
}
|
|
1873
|
+
if (options.quiet) {
|
|
1874
|
+
writeStdout(filePath);
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
writeStdout(response.message);
|
|
1878
|
+
writeStdout(
|
|
1879
|
+
`Saved ${links.length} link${links.length === 1 ? "" : "s"} to ${filePath}.`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
async function listLinks(options) {
|
|
1883
|
+
const config = await getAuthConfig(process.env);
|
|
1884
|
+
const response = await new ApiClient(config).listUrls({
|
|
1885
|
+
page: options.page,
|
|
1886
|
+
limit: options.limit,
|
|
1887
|
+
search: options.search,
|
|
1888
|
+
sortBy: options.sortBy,
|
|
1889
|
+
sortOrder: options.sortOrder
|
|
1890
|
+
});
|
|
1891
|
+
const links = extractLinks(response.data);
|
|
1892
|
+
if (options.json) {
|
|
1893
|
+
writeJson(response);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (options.quiet) {
|
|
1897
|
+
for (const link of links) {
|
|
1898
|
+
const value = getQuietLinkValue(link);
|
|
1899
|
+
if (value) {
|
|
1900
|
+
writeStdout(value);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
writeStdout(response.message);
|
|
1906
|
+
writeStdout(formatLinksTable(links));
|
|
1907
|
+
writeStdout(formatListSummary(response.data, links.length));
|
|
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
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/commands/login.ts
|
|
1945
|
+
async function login(options) {
|
|
1946
|
+
const credentials = getLoginConfig(options, process.env);
|
|
1947
|
+
const client = new ApiClient(credentials);
|
|
1948
|
+
const response = await client.whoami();
|
|
1949
|
+
await new ConfigStore().save(credentials);
|
|
1950
|
+
const responseBody = {
|
|
1951
|
+
success: true,
|
|
1952
|
+
message: `Saved credentials for ${credentials.apiBaseUrl}.`,
|
|
1953
|
+
data: {
|
|
1954
|
+
apiBaseUrl: credentials.apiBaseUrl,
|
|
1955
|
+
user: response.data
|
|
1956
|
+
},
|
|
1957
|
+
timestamp: response.timestamp
|
|
1958
|
+
};
|
|
1959
|
+
if (options.json) {
|
|
1960
|
+
writeJson(responseBody);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (options.quiet) {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
writeStdout(`Saved credentials for ${credentials.apiBaseUrl}`);
|
|
1967
|
+
writeStdout(`Authenticated as ${userLabel(response.data)}`);
|
|
1968
|
+
writeStdout(userTable(response.data));
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/commands/logout.ts
|
|
1972
|
+
function hasEnvConfig(env) {
|
|
1973
|
+
return Boolean(env.PEAKURL_BASE_URL?.trim() || env.PEAKURL_API_KEY?.trim());
|
|
1974
|
+
}
|
|
1975
|
+
async function logout(options) {
|
|
1976
|
+
const store = new ConfigStore();
|
|
1977
|
+
const saved = await store.load();
|
|
1978
|
+
const removed = await store.clear();
|
|
1979
|
+
const envConfig = hasEnvConfig(process.env);
|
|
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.";
|
|
1981
|
+
const responseBody = {
|
|
1982
|
+
success: true,
|
|
1983
|
+
message,
|
|
1984
|
+
data: {
|
|
1985
|
+
removed,
|
|
1986
|
+
apiBaseUrl: saved?.apiBaseUrl,
|
|
1987
|
+
envCredentialsActive: envConfig
|
|
1988
|
+
},
|
|
1989
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1990
|
+
};
|
|
1991
|
+
if (options.json) {
|
|
1992
|
+
writeJson(responseBody);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (options.quiet) {
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
writeStdout(message);
|
|
1999
|
+
if (envConfig) {
|
|
2000
|
+
writeStdout(
|
|
2001
|
+
"Environment credentials in PEAKURL_BASE_URL or PEAKURL_API_KEY still apply in this shell."
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
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));
|
|
992
2020
|
}
|
|
993
2021
|
|
|
994
2022
|
// src/commands/update.ts
|
|
995
|
-
async function
|
|
996
|
-
const
|
|
2023
|
+
async function checkUpdate(options, currentVersion) {
|
|
2024
|
+
const status2 = await getUpdateStatus(currentVersion, process.env, {
|
|
997
2025
|
forceRefresh: true
|
|
998
2026
|
});
|
|
999
2027
|
const responseBody = {
|
|
1000
2028
|
success: true,
|
|
1001
|
-
message:
|
|
2029
|
+
message: status2.isOutdated ? `A newer PeakURL CLI version is available (${status2.latestVersion}).` : `PeakURL CLI ${status2.currentVersion} is up to date.`,
|
|
1002
2030
|
data: {
|
|
1003
|
-
currentVersion:
|
|
1004
|
-
latestVersion:
|
|
1005
|
-
isOutdated:
|
|
1006
|
-
installCommand:
|
|
2031
|
+
currentVersion: status2.currentVersion,
|
|
2032
|
+
latestVersion: status2.latestVersion,
|
|
2033
|
+
isOutdated: status2.isOutdated,
|
|
2034
|
+
installCommand: status2.installCommand,
|
|
1007
2035
|
checkOnly: Boolean(options.check)
|
|
1008
2036
|
},
|
|
1009
2037
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1012,44 +2040,124 @@ async function updateCommand(options, currentVersion) {
|
|
|
1012
2040
|
writeJson(responseBody);
|
|
1013
2041
|
return;
|
|
1014
2042
|
}
|
|
1015
|
-
if (!
|
|
2043
|
+
if (!status2.isOutdated) {
|
|
1016
2044
|
if (!options.quiet) {
|
|
1017
|
-
writeStdout(`PeakURL CLI ${
|
|
2045
|
+
writeStdout(`PeakURL CLI ${status2.currentVersion} is up to date.`);
|
|
1018
2046
|
}
|
|
1019
2047
|
return;
|
|
1020
2048
|
}
|
|
1021
2049
|
if (options.quiet) {
|
|
1022
|
-
writeStdout(
|
|
2050
|
+
writeStdout(status2.installCommand);
|
|
1023
2051
|
return;
|
|
1024
2052
|
}
|
|
1025
2053
|
writeNoticeBox(
|
|
1026
2054
|
"Update Available",
|
|
1027
2055
|
[
|
|
1028
|
-
`peakurl ${
|
|
1029
|
-
`Run: ${
|
|
2056
|
+
`peakurl ${status2.currentVersion} -> ${status2.latestVersion}`,
|
|
2057
|
+
`Run: ${status2.installCommand}`
|
|
1030
2058
|
],
|
|
1031
2059
|
"stdout"
|
|
1032
2060
|
);
|
|
1033
2061
|
}
|
|
1034
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
|
+
|
|
1035
2143
|
// src/commands/whoami.ts
|
|
1036
|
-
async function
|
|
1037
|
-
const config = await
|
|
1038
|
-
const response = await new
|
|
2144
|
+
async function whoami(options) {
|
|
2145
|
+
const config = await getAuthConfig(process.env);
|
|
2146
|
+
const response = await new ApiClient(config).whoami();
|
|
1039
2147
|
if (options.json) {
|
|
1040
2148
|
writeJson(response);
|
|
1041
2149
|
return;
|
|
1042
2150
|
}
|
|
1043
2151
|
if (options.quiet) {
|
|
1044
|
-
writeStdout(
|
|
2152
|
+
writeStdout(userValue(response.data));
|
|
1045
2153
|
return;
|
|
1046
2154
|
}
|
|
1047
2155
|
writeStdout(response.message);
|
|
1048
|
-
writeStdout(
|
|
2156
|
+
writeStdout(userTable(response.data));
|
|
1049
2157
|
}
|
|
1050
2158
|
|
|
1051
2159
|
// src/index.ts
|
|
1052
|
-
function
|
|
2160
|
+
function parseNumber(label) {
|
|
1053
2161
|
return (value) => {
|
|
1054
2162
|
const parsed = Number.parseInt(value, 10);
|
|
1055
2163
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -1062,10 +2170,23 @@ function parsePositiveInteger(label) {
|
|
|
1062
2170
|
}
|
|
1063
2171
|
async function getCliVersion() {
|
|
1064
2172
|
const packageJson = new URL("../package.json", import.meta.url);
|
|
1065
|
-
const content = await
|
|
2173
|
+
const content = await readFile3(packageJson, "utf8");
|
|
1066
2174
|
const parsed = JSON.parse(content);
|
|
1067
2175
|
return parsed.version || "0.0.0";
|
|
1068
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
|
+
}
|
|
1069
2190
|
async function main() {
|
|
1070
2191
|
const program = new Command();
|
|
1071
2192
|
const version = await getCliVersion();
|
|
@@ -1073,17 +2194,23 @@ async function main() {
|
|
|
1073
2194
|
"after",
|
|
1074
2195
|
`
|
|
1075
2196
|
Examples:
|
|
1076
|
-
peakurl login --base-url https://
|
|
2197
|
+
peakurl login --base-url https://example.com/api/v1 --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
|
|
1077
2198
|
peakurl whoami --json
|
|
2199
|
+
peakurl logout
|
|
2200
|
+
peakurl status
|
|
1078
2201
|
peakurl create https://example.com --alias example
|
|
2202
|
+
peakurl import ./links.csv
|
|
2203
|
+
peakurl export --format csv
|
|
1079
2204
|
peakurl list --limit 10
|
|
2205
|
+
peakurl webhook list
|
|
2206
|
+
peakurl webhook create https://example.com/api/webhooks/peakurl --event link.clicked
|
|
1080
2207
|
peakurl update --check
|
|
1081
2208
|
peakurl get example
|
|
1082
2209
|
peakurl delete example --quiet`
|
|
1083
2210
|
).exitOverride();
|
|
1084
2211
|
program.hook("preAction", async (_command, actionCommand) => {
|
|
1085
2212
|
const options = actionCommand.optsWithGlobals();
|
|
1086
|
-
await
|
|
2213
|
+
await checkUpdates({
|
|
1087
2214
|
currentVersion: version,
|
|
1088
2215
|
commandName: actionCommand.name(),
|
|
1089
2216
|
options,
|
|
@@ -1094,30 +2221,63 @@ Examples:
|
|
|
1094
2221
|
"Save PeakURL credentials after verifying them with GET /users/me."
|
|
1095
2222
|
).option(
|
|
1096
2223
|
"--base-url <url>",
|
|
1097
|
-
"PeakURL base URL, for example https://
|
|
1098
|
-
).option("--api-key <token>", "PeakURL API key to store").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(
|
|
1099
|
-
program.command("whoami").description("Show the current authenticated PeakURL user.").option("--json", "Print machine-readable output").option("--quiet", "Print a minimal identity value").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);
|
|
1100
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(
|
|
1101
2230
|
"--status <status>",
|
|
1102
2231
|
"Link status, for example active or paused"
|
|
1103
|
-
).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(
|
|
1104
|
-
program.command("
|
|
1105
|
-
|
|
1106
|
-
|
|
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);
|
|
1107
2242
|
program.command("update").description(
|
|
1108
2243
|
"Check for a newer CLI version and print the npm command to install it."
|
|
1109
2244
|
).option(
|
|
1110
2245
|
"--check",
|
|
1111
2246
|
"Alias for checking update status without changing anything"
|
|
1112
|
-
).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);
|
|
1113
2257
|
try {
|
|
1114
2258
|
await program.parseAsync(process.argv);
|
|
1115
2259
|
} catch (error) {
|
|
1116
2260
|
if (error instanceof CommanderError) {
|
|
1117
2261
|
process.exit(error.exitCode);
|
|
1118
2262
|
}
|
|
1119
|
-
const cliError =
|
|
1120
|
-
|
|
2263
|
+
const cliError = ensureCliError(error);
|
|
2264
|
+
if (cliError.kind === "auth_required") {
|
|
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
|
+
);
|
|
2278
|
+
} else {
|
|
2279
|
+
writeStderr(cliError.message);
|
|
2280
|
+
}
|
|
1121
2281
|
process.exit(cliError.exitCode);
|
|
1122
2282
|
}
|
|
1123
2283
|
}
|