peakurl 0.2.0 → 0.3.1

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