peakurl 0.1.1 → 0.2.0

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