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.
Files changed (3) hide show
  1. package/README.md +108 -12
  2. package/bin/peakurl.js +1420 -260
  3. 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 readFile2 } from "fs/promises";
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 wrapCliError(error) {
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 normalizeBaseUrl(value) {
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
- const installPath = pathname.replace(/\/api\/v1$/i, "");
51
- return installPath ? `${parsed.origin}${installPath}` : parsed.origin;
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(baseUrl, path, query) {
54
- const cleanBaseUrl = normalizeBaseUrl(baseUrl);
61
+ function buildApiUrl(apiBaseUrl, path, query) {
62
+ const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
55
63
  const cleanPath = path.replace(/^\/+/, "");
56
- const url = new URL(`api/v1/${cleanPath}`, `${cleanBaseUrl}/`);
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 isEnvelope(value) {
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 formatNetworkError(baseUrl, error) {
112
+ function networkError(apiBaseUrl, error) {
89
113
  if (error instanceof Error && error.message) {
90
- return `Could not reach PeakURL at ${baseUrl}. ${error.message}`;
114
+ return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
91
115
  }
92
- return `Could not reach PeakURL at ${baseUrl}.`;
116
+ return `Could not reach PeakURL at ${apiBaseUrl}.`;
93
117
  }
94
- var PeakUrlApiClient = class {
118
+ var ApiClient = class {
95
119
  /**
96
120
  * Creates a client bound to one resolved credential set.
97
121
  *
98
- * @param config Normalized base URL plus bearer API key.
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.baseUrl, path, query);
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
- formatNetworkError(this.config.baseUrl, error),
196
- 1,
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 (!isEnvelope(parsed)) {
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 defaultConfigPath() {
324
+ function getConfigPath() {
250
325
  return join(envPaths("peakurl", { suffix: "" }).config, CONFIG_FILENAME);
251
326
  }
252
- function defaultStatePath() {
327
+ function getStatePath() {
253
328
  return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
254
329
  }
255
- async function prepareFileDirectory(filePath) {
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 = defaultConfigPath()) {
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
- if (typeof parsed.baseUrl !== "string" || typeof parsed.apiKey !== "string") {
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
- baseUrl: parsed.baseUrl,
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 prepareFileDirectory(this.filePath);
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 = defaultStatePath()) {
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 prepareFileDirectory(this.filePath);
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
- function resolveLoginConfig(input, env) {
375
- const baseUrl = input.baseUrl?.trim() || env.PEAKURL_BASE_URL?.trim();
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 (!baseUrl || !apiKey) {
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
- baseUrl: normalizeBaseUrl(baseUrl),
504
+ apiBaseUrl: getApiBaseUrl(apiBaseUrl),
384
505
  apiKey
385
506
  };
386
507
  }
387
- async function resolveStoredConfig(env, store = new ConfigStore()) {
508
+ async function getAuthConfig(env, store = new ConfigStore()) {
388
509
  const saved = await store.load();
389
- const baseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.baseUrl;
510
+ const apiBaseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.apiBaseUrl;
390
511
  const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
391
- if (!baseUrl || !apiKey) {
392
- throw new CliError(
393
- "PeakURL credentials are not configured. Run `peakurl login --base-url ... --api-key ...` or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
394
- );
512
+ if (!apiBaseUrl || !apiKey) {
513
+ throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
514
+ kind: "auth_required"
515
+ });
395
516
  }
396
517
  return {
397
- baseUrl: normalizeBaseUrl(baseUrl),
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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, rows, target = "stdout") {
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
- ...rows.map((row) => (row[index] ?? "").length)
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) => `${border.vertical}${cells.map((cell, index) => ` ${(cell ?? "").padEnd(widths[index])} `).join(border.vertical)}${border.vertical}`;
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
- ...rows.map(formatTableRow),
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(/&apos;/g, "'").replace(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/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 objectValue(value) {
1001
+ function asObject(value) {
511
1002
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
512
1003
  }
513
- function stringValue(value) {
1004
+ function asString(value) {
514
1005
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
515
1006
  }
516
- function numberValue(value) {
1007
+ function asNumber(value) {
517
1008
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
518
1009
  }
519
- function pickString(link, keys) {
1010
+ function pickText(link, keys) {
520
1011
  for (const key of keys) {
521
- const value = stringValue(link[key]);
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 extractListMeta(data) {
532
- const record = objectValue(data);
1022
+ function getListMeta(data) {
1023
+ const record = asObject(data);
533
1024
  if (!record) {
534
1025
  return null;
535
1026
  }
536
- const meta = objectValue(record.meta);
1027
+ const meta = asObject(record.meta);
537
1028
  if (meta) {
538
1029
  return {
539
- page: numberValue(meta.page),
540
- limit: numberValue(meta.limit),
541
- totalItems: numberValue(meta.totalItems),
542
- totalPages: numberValue(meta.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: numberValue(record.page),
547
- limit: numberValue(record.limit),
548
- totalItems: numberValue(record.total),
549
- totalPages: numberValue(record.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 = objectValue(data);
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 pickString(link, ["id", "_id", "urlId"]) || (link.id !== void 0 ? String(link.id) : void 0);
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 pickString(link, ["alias", "shortCode", "slug", "code"]);
1062
+ return pickText(link, ["alias", "shortCode", "slug", "code"]);
572
1063
  }
573
1064
  function getLinkShortUrl(link) {
574
- return pickString(link, ["shortUrl", "shortLink", "shortURL", "url"]);
1065
+ return pickText(link, ["shortUrl", "shortLink", "shortURL", "url"]);
575
1066
  }
576
1067
  function getLinkDestination(link) {
577
- return pickString(link, [
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", stringValue(link.title)],
594
- ["Status", stringValue(link.status)],
1084
+ ["Title", asString(link.title)],
1085
+ ["Status", asString(link.status)],
595
1086
  [
596
1087
  "Clicks",
597
- numberValue(link.clicks) === void 0 ? void 0 : String(link.clicks)
1088
+ asNumber(link.clicks) === void 0 ? void 0 : String(link.clicks)
598
1089
  ],
599
- ["Created", stringValue(link.createdAt)],
600
- ["Updated", stringValue(link.updatedAt)]
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 rows = links.map((link) => [
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(stringValue(link.status) || "-", 12)
1105
+ truncate(asString(link.status) || "-", 12)
615
1106
  ]);
616
- return formatTable(headers, rows);
1107
+ return formatTable(headers, rows2);
617
1108
  }
618
1109
  function formatListSummary(data, count) {
619
- const meta = extractListMeta(data);
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/commands/create.ts
633
- function normalizeExpiresAt(value) {
634
- if (!value) {
635
- return void 0;
636
- }
637
- if (Number.isNaN(Date.parse(value))) {
638
- throw new CliError(`Invalid expiration timestamp: ${value}`);
639
- }
640
- return value;
1123
+ // src/lib/status.ts
1124
+ function text3(value) {
1125
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
641
1126
  }
642
- async function createCommand(destinationUrl, options) {
643
- const config = await resolveStoredConfig(process.env);
644
- const response = await new PeakUrlApiClient(config).createUrl({
645
- destinationUrl: normalizeDestinationUrl(destinationUrl),
646
- ...options.alias ? { alias: options.alias } : {},
647
- ...options.title ? { title: options.title } : {},
648
- ...options.password ? { password: options.password } : {},
649
- ...options.status ? { status: options.status } : {},
650
- ...options.expiresAt ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
651
- ...options.utmSource ? { utmSource: options.utmSource } : {},
652
- ...options.utmMedium ? { utmMedium: options.utmMedium } : {},
653
- ...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
654
- ...options.utmTerm ? { utmTerm: options.utmTerm } : {},
655
- ...options.utmContent ? { utmContent: options.utmContent } : {}
656
- });
657
- if (options.json) {
658
- writeJson(response);
659
- return;
1127
+ function integer(value) {
1128
+ if (typeof value === "number" && Number.isFinite(value)) {
1129
+ return value;
660
1130
  }
661
- if (options.quiet) {
662
- writeStdout(getQuietLinkValue(response.data));
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
- writeStdout(response.message);
666
- writeStdout(formatLinkDetails(response.data));
1135
+ return void 0;
667
1136
  }
668
-
669
- // src/commands/delete.ts
670
- async function deleteCommand(idOrAlias, options) {
671
- const config = await resolveStoredConfig(process.env);
672
- const client = new PeakUrlApiClient(config);
673
- const lookupResponse = await client.getUrl(idOrAlias);
674
- const resolvedId = getLinkId(lookupResponse.data);
675
- if (!resolvedId) {
676
- throw new CliError(
677
- "PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
678
- );
1137
+ function flag(value) {
1138
+ if (typeof value === "boolean") {
1139
+ return value;
679
1140
  }
680
- const response = await client.deleteUrl(resolvedId);
681
- if (options.json) {
682
- writeJson(response);
683
- return;
1141
+ if (value === 1 || value === "1") {
1142
+ return true;
684
1143
  }
685
- if (options.quiet) {
686
- return;
1144
+ if (value === 0 || value === "0") {
1145
+ return false;
687
1146
  }
688
- writeStdout(response.message);
1147
+ return void 0;
689
1148
  }
690
-
691
- // src/commands/get.ts
692
- async function getCommand(idOrAlias, options) {
693
- const config = await resolveStoredConfig(process.env);
694
- const response = await new PeakUrlApiClient(config).getUrl(idOrAlias);
695
- if (options.json) {
696
- writeJson(response);
697
- return;
698
- }
699
- if (options.quiet) {
700
- writeStdout(getQuietLinkValue(response.data));
701
- return;
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
- writeStdout(response.message);
704
- writeStdout(formatLinkDetails(response.data));
1154
+ return normalized ? yes : no;
705
1155
  }
706
-
707
- // src/commands/list.ts
708
- async function listCommand(options) {
709
- const config = await resolveStoredConfig(process.env);
710
- const response = await new PeakUrlApiClient(config).listUrls({
711
- page: options.page,
712
- limit: options.limit,
713
- search: options.search,
714
- sortBy: options.sortBy,
715
- sortOrder: options.sortOrder
716
- });
717
- const links = extractLinks(response.data);
718
- if (options.json) {
719
- writeJson(response);
720
- return;
1156
+ function formatState(value) {
1157
+ const normalized = text3(value)?.toLowerCase();
1158
+ if (!normalized) {
1159
+ return void 0;
721
1160
  }
722
- if (options.quiet) {
723
- for (const link of links) {
724
- const value = getQuietLinkValue(link);
725
- if (value) {
726
- writeStdout(value);
727
- }
728
- }
729
- return;
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
- // src/lib/users.ts
737
- function stringValue2(value) {
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 getUserLabel(user) {
741
- const fullName = [stringValue2(user.firstName), stringValue2(user.lastName)].filter(Boolean).join(" ");
742
- return fullName || stringValue2(user.username) || stringValue2(user.email) || String(user.id ?? "unknown");
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 getQuietUserValue(user) {
745
- return stringValue2(user.username) || stringValue2(user.email) || String(user.id ?? "");
1191
+ function formatSeconds(value) {
1192
+ const normalized = integer(value);
1193
+ return normalized === void 0 ? text3(value) : `${normalized} seconds`;
746
1194
  }
747
- function formatUserDetails(user) {
748
- const lines = [
749
- ["Name", getUserLabel(user)],
750
- ["Username", stringValue2(user.username)],
751
- ["Email", stringValue2(user.email)],
752
- ["Role", stringValue2(user.role)],
753
- ["ID", user.id === void 0 ? void 0 : String(user.id)]
754
- ].filter((entry) => Boolean(entry[1]));
755
- return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
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
- // src/commands/login.ts
759
- async function loginCommand(options) {
760
- const credentials = resolveLoginConfig(options, process.env);
761
- const client = new PeakUrlApiClient(credentials);
762
- const response = await client.whoami();
763
- await new ConfigStore().save(credentials);
764
- const responseBody = {
765
- success: true,
766
- message: `Saved credentials for ${credentials.baseUrl}.`,
767
- data: {
768
- baseUrl: credentials.baseUrl,
769
- user: response.data
770
- },
771
- timestamp: response.timestamp
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
- if (options.json) {
774
- writeJson(responseBody);
775
- return;
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
- if (options.quiet) {
778
- return;
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
- writeStdout(`Saved credentials for ${credentials.baseUrl}`);
781
- writeStdout(`Authenticated as ${getUserLabel(response.data)}`);
782
- writeStdout(formatUserDetails(response.data));
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 fetchLatestPackageVersion(env) {
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 getCachedUpdateState(store) {
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 getLatestPackageVersion(env, options) {
1578
+ async function loadLatestVersion(env, options) {
924
1579
  const store = options?.store ?? new StateStore();
925
- const updateState = await getCachedUpdateState(store);
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 fetchLatestPackageVersion(env);
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 getLatestPackageVersion(env, {
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 writeUpdateNotice(status) {
1615
+ function showUpdateNotice(status2) {
961
1616
  writeNoticeBox("Update Available", [
962
- `${PACKAGE_NAME} ${status.currentVersion} -> ${status.latestVersion}`,
963
- `Run: ${status.installCommand}`
1617
+ `${PACKAGE_NAME} ${status2.currentVersion} -> ${status2.latestVersion}`,
1618
+ `Run: ${status2.installCommand}`
964
1619
  ]);
965
1620
  }
966
- async function maybeShowUpdateNotice(options) {
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 getCachedUpdateState(store);
975
- const status = await getUpdateStatus(options.currentVersion, options.env, {
1629
+ const updateState = await getUpdateState(store);
1630
+ const status2 = await getUpdateStatus(options.currentVersion, options.env, {
976
1631
  store
977
1632
  });
978
- if (!status.isOutdated) {
1633
+ if (!status2.isOutdated) {
979
1634
  return;
980
1635
  }
981
1636
  const lastNotifiedAt = parseTime(updateState.lastNotifiedAt);
982
- const alreadyNotifiedForVersion = updateState.lastNotifiedVersion === status.latestVersion;
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: status.latestVersion
1644
+ lastNotifiedVersion: status2.latestVersion
990
1645
  });
991
- writeUpdateNotice(status);
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 updateCommand(options, currentVersion) {
996
- const status = await getUpdateStatus(currentVersion, process.env, {
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: status.isOutdated ? `A newer PeakURL CLI version is available (${status.latestVersion}).` : `PeakURL CLI ${status.currentVersion} is up to date.`,
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: status.currentVersion,
1004
- latestVersion: status.latestVersion,
1005
- isOutdated: status.isOutdated,
1006
- installCommand: status.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 (!status.isOutdated) {
2043
+ if (!status2.isOutdated) {
1016
2044
  if (!options.quiet) {
1017
- writeStdout(`PeakURL CLI ${status.currentVersion} is up to date.`);
2045
+ writeStdout(`PeakURL CLI ${status2.currentVersion} is up to date.`);
1018
2046
  }
1019
2047
  return;
1020
2048
  }
1021
2049
  if (options.quiet) {
1022
- writeStdout(status.installCommand);
2050
+ writeStdout(status2.installCommand);
1023
2051
  return;
1024
2052
  }
1025
2053
  writeNoticeBox(
1026
2054
  "Update Available",
1027
2055
  [
1028
- `peakurl ${status.currentVersion} -> ${status.latestVersion}`,
1029
- `Run: ${status.installCommand}`
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 whoamiCommand(options) {
1037
- const config = await resolveStoredConfig(process.env);
1038
- const response = await new PeakUrlApiClient(config).whoami();
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(getQuietUserValue(response.data));
2152
+ writeStdout(userValue(response.data));
1045
2153
  return;
1046
2154
  }
1047
2155
  writeStdout(response.message);
1048
- writeStdout(formatUserDetails(response.data));
2156
+ writeStdout(userTable(response.data));
1049
2157
  }
1050
2158
 
1051
2159
  // src/index.ts
1052
- function parsePositiveInteger(label) {
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 readFile2(packageJson, "utf8");
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://peakurl.org --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
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 maybeShowUpdateNotice({
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://peakurl.org"
1098
- ).option("--api-key <token>", "PeakURL API key to store").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(loginCommand);
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(whoamiCommand);
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(createCommand);
1104
- program.command("list").description("List PeakURL short links.").option("--page <number>", "Page number", parsePositiveInteger("page")).option("--limit <number>", "Page size", parsePositiveInteger("limit")).option("--search <query>", "Search term").option("--sort-by <field>", "Sort field").option("--sort-order <order>", "Sort order, for example asc or desc").option("--json", "Print machine-readable output").option("--quiet", "Print a minimal per-link value").action(listCommand);
1105
- program.command("get").description("Fetch a single PeakURL short link by id or alias.").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Print only the short URL").action(getCommand);
1106
- program.command("delete").description("Delete a PeakURL short link by id or alias.").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteCommand);
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) => updateCommand(options, version));
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 = wrapCliError(error);
1120
- writeStderr(cliError.message);
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
  }