peakurl 0.3.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/peakurl.js CHANGED
@@ -4,9 +4,13 @@
4
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";
7
+ // src/commands/core.ts
8
+ import { cwd } from "process";
9
+
10
+ // src/config/store.ts
11
+ import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
12
+ import { dirname, join } from "path";
13
+ import envPaths from "env-paths";
10
14
 
11
15
  // src/lib/errors.ts
12
16
  var CliError = class extends Error {
@@ -29,296 +33,7 @@ function ensureCliError(error) {
29
33
  return new CliError("Unexpected error.");
30
34
  }
31
35
 
32
- // src/lib/url.ts
33
- function validateHttpUrl(parsed, label) {
34
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
35
- throw new CliError(`${label} must use http or https.`);
36
- }
37
- if (parsed.username || parsed.password) {
38
- throw new CliError(`${label} must not include embedded credentials.`);
39
- }
40
- }
41
- function getApiBaseUrl(value) {
42
- const input = value.trim();
43
- if (!input) {
44
- throw new CliError("A PeakURL API base URL is required.");
45
- }
46
- let parsed;
47
- try {
48
- parsed = new URL(input);
49
- } catch {
50
- throw new CliError(`Invalid API base URL: ${value}`);
51
- }
52
- validateHttpUrl(parsed, "PeakURL API base URL");
53
- parsed.hash = "";
54
- parsed.search = "";
55
- const pathname = parsed.pathname.replace(/\/+$/, "");
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}`;
60
- }
61
- function buildApiUrl(apiBaseUrl, path, query) {
62
- const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
63
- const cleanPath = path.replace(/^\/+/, "");
64
- const url = new URL(cleanPath, `${cleanBaseUrl}/`);
65
- for (const [key, value] of Object.entries(query ?? {})) {
66
- if (value === void 0 || value === "") {
67
- continue;
68
- }
69
- url.searchParams.set(key, String(value));
70
- }
71
- return url.toString();
72
- }
73
- function normalizeDestinationUrl(value) {
74
- const input = value.trim();
75
- if (!input) {
76
- throw new CliError("A destination URL is required.");
77
- }
78
- try {
79
- const parsed = new URL(input);
80
- validateHttpUrl(parsed, "Destination URL");
81
- return parsed.toString();
82
- } catch (error) {
83
- if (error instanceof CliError) {
84
- throw error;
85
- }
86
- throw new CliError(`Invalid destination URL: ${value}`);
87
- }
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
- }
105
-
106
- // src/api/client.ts
107
- function isApiResponse(value) {
108
- return Boolean(
109
- value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
110
- );
111
- }
112
- function networkError(apiBaseUrl, error) {
113
- if (error instanceof Error && error.message) {
114
- return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
115
- }
116
- return `Could not reach PeakURL at ${apiBaseUrl}.`;
117
- }
118
- var ApiClient = class {
119
- /**
120
- * Creates a client bound to one resolved credential set.
121
- *
122
- * @param config Explicit API base URL plus bearer API key.
123
- */
124
- constructor(config) {
125
- this.config = config;
126
- }
127
- config;
128
- /**
129
- * Loads the currently authenticated user.
130
- *
131
- * PeakURL accepts bearer API keys on `GET /users/me`, which is also the
132
- * CLI login verification flow.
133
- *
134
- * @returns API response envelope containing the authenticated user.
135
- */
136
- whoami() {
137
- return this.request("GET", "users/me");
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
- }
147
- /**
148
- * Creates a short URL.
149
- *
150
- * @param payload Request body accepted by `POST /api/v1/urls`.
151
- * @returns API response envelope containing the created link.
152
- */
153
- createUrl(payload) {
154
- return this.request("POST", "urls", payload);
155
- }
156
- /**
157
- * Lists short URLs with optional pagination and filtering.
158
- *
159
- * The current PeakURL app returns `{ items, meta }` under `data`, but the
160
- * CLI keeps a slightly broader compatibility type for future-proofing.
161
- *
162
- * @param query Optional query-string values.
163
- * @returns API response envelope containing list data.
164
- */
165
- listUrls(query) {
166
- return this.request("GET", "urls", void 0, query);
167
- }
168
- /**
169
- * Exports the full accessible link dataset for the authenticated user.
170
- *
171
- * @param query Optional search and sort values.
172
- * @returns API response envelope containing the full export payload.
173
- */
174
- exportUrls(query) {
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
- }
191
- /**
192
- * Loads a single short URL by identifier or alias.
193
- *
194
- * PeakURL resolves IDs, short codes, and aliases through the same route.
195
- *
196
- * @param idOrAlias Link identifier, short code, or alias.
197
- * @returns API response envelope containing the resolved link.
198
- */
199
- getUrl(idOrAlias) {
200
- return this.request(
201
- "GET",
202
- `urls/${encodeURIComponent(idOrAlias)}`
203
- );
204
- }
205
- /**
206
- * Deletes a short URL by its stable row ID.
207
- *
208
- * The current PeakURL backend delete route expects the row ID. The CLI can
209
- * still accept an alias at the command layer by resolving it first.
210
- *
211
- * @param id Stable link row ID.
212
- * @returns API response envelope containing the deletion result.
213
- */
214
- deleteUrl(id) {
215
- return this.request(
216
- "DELETE",
217
- `urls/${encodeURIComponent(id)}`
218
- );
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
- }
249
- /**
250
- * Performs one authenticated API request and normalizes the response.
251
- *
252
- * @param method HTTP method to send.
253
- * @param path Route path relative to `/api/v1`.
254
- * @param body Optional JSON body.
255
- * @param query Optional query-string values.
256
- * @returns Parsed PeakURL response envelope.
257
- * @throws {CliError} When the network request fails or the API returns an error.
258
- */
259
- async request(method, path, body, query) {
260
- const url = buildApiUrl(this.config.apiBaseUrl, path, query);
261
- let response;
262
- try {
263
- response = await fetch(url, {
264
- method,
265
- headers: {
266
- Accept: "application/json",
267
- Authorization: `Bearer ${this.config.apiKey}`,
268
- ...body ? { "Content-Type": "application/json" } : {}
269
- },
270
- body: body ? JSON.stringify(body) : void 0
271
- });
272
- } catch (error) {
273
- throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
274
- cause: error instanceof Error ? error : void 0
275
- });
276
- }
277
- const rawText = await response.text();
278
- if (!rawText) {
279
- if (!response.ok) {
280
- throw new CliError(
281
- `PeakURL request failed with HTTP ${response.status}.`
282
- );
283
- }
284
- return {
285
- success: true,
286
- message: "Request completed.",
287
- data: void 0,
288
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
289
- };
290
- }
291
- let parsed;
292
- try {
293
- parsed = JSON.parse(rawText);
294
- } catch {
295
- if (!response.ok) {
296
- throw new CliError(
297
- `PeakURL request failed with HTTP ${response.status}.`
298
- );
299
- }
300
- throw new CliError("PeakURL returned an invalid JSON response.");
301
- }
302
- if (!isApiResponse(parsed)) {
303
- throw new CliError(
304
- "PeakURL returned an unexpected response envelope."
305
- );
306
- }
307
- if (!response.ok || !parsed.success) {
308
- const statusCode = response.status === 401 ? 2 : 1;
309
- throw new CliError(
310
- parsed.message || `PeakURL request failed with HTTP ${response.status}.`,
311
- statusCode
312
- );
313
- }
314
- return parsed;
315
- }
316
- };
317
-
318
36
  // src/config/store.ts
319
- import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
320
- import { dirname, join } from "path";
321
- import envPaths from "env-paths";
322
37
  var CONFIG_FILENAME = "config.json";
323
38
  var STATE_FILENAME = "state.json";
324
39
  function getConfigPath() {
@@ -469,6 +184,80 @@ var StateStore = class {
469
184
  }
470
185
  };
471
186
 
187
+ // src/lib/url.ts
188
+ function validateHttpUrl(parsed, label) {
189
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
190
+ throw new CliError(`${label} must use http or https.`);
191
+ }
192
+ if (parsed.username || parsed.password) {
193
+ throw new CliError(`${label} must not include embedded credentials.`);
194
+ }
195
+ }
196
+ function getApiBaseUrl(value) {
197
+ const input = value.trim();
198
+ if (!input) {
199
+ throw new CliError("A PeakURL API base URL is required.");
200
+ }
201
+ let parsed;
202
+ try {
203
+ parsed = new URL(input);
204
+ } catch {
205
+ throw new CliError(`Invalid API base URL: ${value}`);
206
+ }
207
+ validateHttpUrl(parsed, "PeakURL API base URL");
208
+ parsed.hash = "";
209
+ parsed.search = "";
210
+ const pathname = parsed.pathname.replace(/\/+$/, "");
211
+ if (!/\/api\/v1$/i.test(pathname)) {
212
+ throw new CliError("PeakURL API base URL must end with /api/v1.");
213
+ }
214
+ return `${parsed.origin}${pathname}`;
215
+ }
216
+ function buildApiUrl(apiBaseUrl, path, query) {
217
+ const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
218
+ const cleanPath = path.replace(/^\/+/, "");
219
+ const url = new URL(cleanPath, `${cleanBaseUrl}/`);
220
+ for (const [key, value] of Object.entries(query ?? {})) {
221
+ if (value === void 0 || value === "") {
222
+ continue;
223
+ }
224
+ url.searchParams.set(key, String(value));
225
+ }
226
+ return url.toString();
227
+ }
228
+ function normalizeDestinationUrl(value) {
229
+ const input = value.trim();
230
+ if (!input) {
231
+ throw new CliError("A destination URL is required.");
232
+ }
233
+ try {
234
+ const parsed = new URL(input);
235
+ validateHttpUrl(parsed, "Destination URL");
236
+ return parsed.toString();
237
+ } catch (error) {
238
+ if (error instanceof CliError) {
239
+ throw error;
240
+ }
241
+ throw new CliError(`Invalid destination URL: ${value}`);
242
+ }
243
+ }
244
+ function normalizeWebhookUrl(value) {
245
+ const input = value.trim();
246
+ if (!input) {
247
+ throw new CliError("A webhook URL is required.");
248
+ }
249
+ try {
250
+ const parsed = new URL(input);
251
+ validateHttpUrl(parsed, "Webhook URL");
252
+ return parsed.toString();
253
+ } catch (error) {
254
+ if (error instanceof CliError) {
255
+ throw error;
256
+ }
257
+ throw new CliError(`Invalid webhook URL: ${value}`);
258
+ }
259
+ }
260
+
472
261
  // src/lib/auth.ts
473
262
  var AUTH_REQUIRED_MESSAGE = "PeakURL credentials are not configured.";
474
263
  var EXAMPLE_BASE_URL = "https://example.com/api/v1";
@@ -497,131 +286,44 @@ function getLoginConfig(input, env) {
497
286
  const apiKey = input.apiKey?.trim() || env.PEAKURL_API_KEY?.trim();
498
287
  if (!apiBaseUrl || !apiKey) {
499
288
  throw new CliError(
500
- "Missing credentials. Provide --base-url and --api-key, or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
501
- );
502
- }
503
- return {
504
- apiBaseUrl: getApiBaseUrl(apiBaseUrl),
505
- apiKey
506
- };
507
- }
508
- async function getAuthConfig(env, store = new ConfigStore()) {
509
- const saved = await store.load();
510
- const apiBaseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.apiBaseUrl;
511
- const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
512
- if (!apiBaseUrl || !apiKey) {
513
- throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
514
- kind: "auth_required"
515
- });
516
- }
517
- return {
518
- apiBaseUrl: getApiBaseUrl(apiBaseUrl),
519
- apiKey
520
- };
521
- }
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);
289
+ "Missing credentials. Provide --base-url and --api-key, or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
290
+ );
593
291
  }
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
- `;
292
+ return {
293
+ apiBaseUrl: getApiBaseUrl(apiBaseUrl),
294
+ apiKey
295
+ };
296
+ }
297
+ async function getAuthConfig(env, store = new ConfigStore()) {
298
+ const saved = await store.load();
299
+ const apiBaseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.apiBaseUrl;
300
+ const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
301
+ if (!apiBaseUrl || !apiKey) {
302
+ throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
303
+ kind: "auth_required"
304
+ });
612
305
  }
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
- `;
306
+ return {
307
+ apiBaseUrl: getApiBaseUrl(apiBaseUrl),
308
+ apiKey
309
+ };
621
310
  }
622
311
 
623
- // src/lib/imports.ts
624
- import { readFile as readFile2 } from "fs/promises";
312
+ // src/lib/core.ts
313
+ import { createHash } from "crypto";
314
+ import {
315
+ chmod as chmod2,
316
+ copyFile,
317
+ lstat,
318
+ mkdir as mkdir2,
319
+ mkdtemp,
320
+ rm,
321
+ writeFile as writeFile2
322
+ } from "fs/promises";
323
+ import { tmpdir } from "os";
324
+ import { dirname as dirname2, join as join2, resolve } from "path";
325
+ import { posix as pathPosix } from "path";
326
+ import { inflateRawSync } from "zlib";
625
327
 
626
328
  // src/lib/output.ts
627
329
  function writeStdout(message = "") {
@@ -632,13 +334,27 @@ function writeStderr(message = "") {
632
334
  process.stderr.write(`${message}
633
335
  `);
634
336
  }
337
+ function outputStream(target) {
338
+ return target === "stdout" ? process.stdout : process.stderr;
339
+ }
340
+ function useColor(target) {
341
+ return Boolean(outputStream(target).isTTY && !process.env.NO_COLOR);
342
+ }
343
+ function successLine(message, target = "stdout") {
344
+ const label = useColor(target) ? "\x1B[32mSuccess\x1B[39m" : "Success";
345
+ return `${label}: ${message}`;
346
+ }
347
+ function errorLine(message, target = "stderr") {
348
+ const label = useColor(target) ? "\x1B[31mError\x1B[39m" : "Error";
349
+ return `${label}: ${message}`;
350
+ }
635
351
  function writeNoticeBox(title, lines, target = "stderr") {
636
352
  const contentLines = lines.length > 0 ? lines : [""];
637
353
  const width = Math.max(
638
354
  title.length,
639
355
  ...contentLines.map((line) => line.length)
640
356
  );
641
- const stream = target === "stdout" ? process.stdout : process.stderr;
357
+ const stream = outputStream(target);
642
358
  const useTuiBox = stream.isTTY;
643
359
  const border = useTuiBox ? {
644
360
  topLeft: "\u250C",
@@ -674,7 +390,7 @@ function writeNoticeBox(title, lines, target = "stderr") {
674
390
  writeLine(bottomBorder);
675
391
  }
676
392
  function formatTable(headers, rows2, target = "stdout") {
677
- const stream = target === "stdout" ? process.stdout : process.stderr;
393
+ const stream = outputStream(target);
678
394
  const useTuiBox = stream.isTTY;
679
395
  const border = useTuiBox ? {
680
396
  topLeft: "\u250C",
@@ -710,7 +426,7 @@ function formatTable(headers, rows2, target = "stdout") {
710
426
  )
711
427
  )
712
428
  );
713
- const formatTableBorder = (left, join2, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join2)}${right}`;
429
+ const formatTableBorder = (left, join3, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join3)}${right}`;
714
430
  const formatTableRow = (cells) => {
715
431
  const linesByCell = cells.map(getLines);
716
432
  const rowHeight = Math.max(...linesByCell.map((lines) => lines.length));
@@ -735,13 +451,435 @@ function formatTable(headers, rows2, target = "stdout") {
735
451
  border.bottomJunction,
736
452
  border.bottomRight
737
453
  )
738
- ].join("\n");
739
- }
740
- function writeJson(value) {
741
- writeStdout(JSON.stringify(value, null, 2));
454
+ ].join("\n");
455
+ }
456
+ function formatDetailsTable(rows2, target = "stdout") {
457
+ return formatTable(["Detail", "Information"], rows2, target);
458
+ }
459
+ function writeJson(value) {
460
+ writeStdout(JSON.stringify(value, null, 2));
461
+ }
462
+
463
+ // src/lib/core.ts
464
+ var DEFAULT_RELEASE_API_URL = "https://api.peakurl.org/v1/update";
465
+ var DEFAULT_CORE_PACKAGE_URL = "https://peakurl.org/latest.zip";
466
+ var EOCD_SIGNATURE = 101010256;
467
+ var CENTRAL_DIRECTORY_SIGNATURE = 33639248;
468
+ var LOCAL_FILE_SIGNATURE = 67324752;
469
+ var ZIP_UTF8_FLAG = 2048;
470
+ var ZIP_ENCRYPTED_FLAG = 1;
471
+ function getReleaseApiUrl(env) {
472
+ const candidate = env.PEAKURL_RELEASE_API_URL?.trim();
473
+ return validateUrl(candidate || DEFAULT_RELEASE_API_URL, "release feed");
474
+ }
475
+ function getCorePackageUrl(env) {
476
+ const candidate = env.PEAKURL_CORE_PACKAGE_URL?.trim();
477
+ return validateUrl(
478
+ candidate || DEFAULT_CORE_PACKAGE_URL,
479
+ "package download"
480
+ );
481
+ }
482
+ function asString(value) {
483
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
484
+ }
485
+ function validateUrl(value, label) {
486
+ let parsed;
487
+ try {
488
+ parsed = new URL(value);
489
+ } catch {
490
+ throw new CliError(`PeakURL ${label} URL is invalid.`);
491
+ }
492
+ if (parsed.protocol === "data:") {
493
+ return parsed.toString();
494
+ }
495
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password) {
496
+ throw new CliError(`PeakURL ${label} URL is invalid.`);
497
+ }
498
+ return parsed.toString();
499
+ }
500
+ function normalizeSha256(value) {
501
+ const candidate = asString(value)?.toLowerCase();
502
+ if (!candidate || !/^[a-f0-9]{64}$/.test(candidate)) {
503
+ throw new CliError(
504
+ "PeakURL release metadata is missing a valid SHA-256 checksum."
505
+ );
506
+ }
507
+ return candidate;
508
+ }
509
+ function assertBufferRange(buffer, offset, length, label) {
510
+ if (offset < 0 || length < 0 || offset + length > buffer.length) {
511
+ throw new CliError(`PeakURL core package has an invalid ${label}.`);
512
+ }
513
+ }
514
+ function findEndOfCentralDirectory(buffer) {
515
+ const start = Math.max(0, buffer.length - 65535 - 22);
516
+ for (let index = buffer.length - 22; index >= start; index -= 1) {
517
+ if (buffer.readUInt32LE(index) === EOCD_SIGNATURE) {
518
+ return index;
519
+ }
520
+ }
521
+ throw new CliError("PeakURL core package is not a valid ZIP archive.");
522
+ }
523
+ function normalizeZipPath(name) {
524
+ if (!name || name.includes("\0") || name.includes("\\")) {
525
+ throw new CliError(
526
+ "PeakURL core package contains an invalid file path."
527
+ );
528
+ }
529
+ const normalized = pathPosix.normalize(name);
530
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) {
531
+ throw new CliError(
532
+ "PeakURL core package contains an unsafe file path."
533
+ );
534
+ }
535
+ return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
536
+ }
537
+ function getZipMode(externalAttributes) {
538
+ return externalAttributes >>> 16 & 65535;
539
+ }
540
+ function isSymbolicLink(mode) {
541
+ return (mode & 61440) === 40960;
542
+ }
543
+ function parseZipEntries(buffer) {
544
+ const endOfCentralDirectoryOffset = findEndOfCentralDirectory(buffer);
545
+ const entryCount = buffer.readUInt16LE(endOfCentralDirectoryOffset + 10);
546
+ const directoryOffset = buffer.readUInt32LE(
547
+ endOfCentralDirectoryOffset + 16
548
+ );
549
+ if (entryCount === 65535 || directoryOffset === 4294967295) {
550
+ throw new CliError(
551
+ "PeakURL core package uses an unsupported ZIP format."
552
+ );
553
+ }
554
+ let cursor = directoryOffset;
555
+ const entries = [];
556
+ const seenPaths = /* @__PURE__ */ new Set();
557
+ for (let index = 0; index < entryCount; index += 1) {
558
+ assertBufferRange(buffer, cursor, 46, "central directory header");
559
+ if (buffer.readUInt32LE(cursor) !== CENTRAL_DIRECTORY_SIGNATURE) {
560
+ throw new CliError(
561
+ "PeakURL core package has a broken ZIP directory."
562
+ );
563
+ }
564
+ const flags = buffer.readUInt16LE(cursor + 8);
565
+ const compressionMethod = buffer.readUInt16LE(cursor + 10);
566
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
567
+ const uncompressedSize = buffer.readUInt32LE(cursor + 24);
568
+ const fileNameLength = buffer.readUInt16LE(cursor + 28);
569
+ const extraFieldLength = buffer.readUInt16LE(cursor + 30);
570
+ const fileCommentLength = buffer.readUInt16LE(cursor + 32);
571
+ const externalAttributes = buffer.readUInt32LE(cursor + 38);
572
+ const localHeaderOffset = buffer.readUInt32LE(cursor + 42);
573
+ const nameOffset = cursor + 46;
574
+ assertBufferRange(
575
+ buffer,
576
+ nameOffset,
577
+ fileNameLength + extraFieldLength + fileCommentLength,
578
+ "central directory entry"
579
+ );
580
+ if (flags & ZIP_ENCRYPTED_FLAG) {
581
+ throw new CliError(
582
+ "PeakURL core package uses unsupported encrypted ZIP entries."
583
+ );
584
+ }
585
+ if (compressionMethod !== 0 && compressionMethod !== 8) {
586
+ throw new CliError(
587
+ "PeakURL core package uses an unsupported ZIP compression method."
588
+ );
589
+ }
590
+ const encoding = flags & ZIP_UTF8_FLAG ? "utf8" : "utf8";
591
+ const rawName = buffer.subarray(nameOffset, nameOffset + fileNameLength).toString(encoding);
592
+ const path = normalizeZipPath(rawName);
593
+ const mode = getZipMode(externalAttributes);
594
+ const directory = rawName.endsWith("/");
595
+ if (isSymbolicLink(mode)) {
596
+ throw new CliError(
597
+ "PeakURL core package contains unsupported symbolic links."
598
+ );
599
+ }
600
+ if (seenPaths.has(path)) {
601
+ throw new CliError(
602
+ "PeakURL core package contains duplicate file paths."
603
+ );
604
+ }
605
+ seenPaths.add(path);
606
+ entries.push({
607
+ path,
608
+ mode,
609
+ isDirectory: directory,
610
+ compressedSize,
611
+ uncompressedSize,
612
+ compressionMethod,
613
+ flags,
614
+ localHeaderOffset
615
+ });
616
+ cursor = nameOffset + fileNameLength + extraFieldLength + fileCommentLength;
617
+ }
618
+ return entries;
619
+ }
620
+ function getLocalFileData(buffer, entry) {
621
+ assertBufferRange(buffer, entry.localHeaderOffset, 30, "local file header");
622
+ if (buffer.readUInt32LE(entry.localHeaderOffset) !== LOCAL_FILE_SIGNATURE) {
623
+ throw new CliError("PeakURL core package has a broken file header.");
624
+ }
625
+ const fileNameLength = buffer.readUInt16LE(entry.localHeaderOffset + 26);
626
+ const extraFieldLength = buffer.readUInt16LE(entry.localHeaderOffset + 28);
627
+ const dataOffset = entry.localHeaderOffset + 30 + fileNameLength + extraFieldLength;
628
+ assertBufferRange(buffer, dataOffset, entry.compressedSize, "file payload");
629
+ return buffer.subarray(dataOffset, dataOffset + entry.compressedSize);
630
+ }
631
+ function extractZipEntry(buffer, entry) {
632
+ const compressed = getLocalFileData(buffer, entry);
633
+ if (entry.compressionMethod === 0) {
634
+ return compressed;
635
+ }
636
+ return inflateRawSync(compressed);
637
+ }
638
+ async function writeZipEntry(buffer, entry, targetPath) {
639
+ const destinationPath = join2(targetPath, entry.path);
640
+ if (entry.isDirectory) {
641
+ await mkdir2(destinationPath, { recursive: true });
642
+ return;
643
+ }
644
+ const content = extractZipEntry(buffer, entry);
645
+ if (content.length !== entry.uncompressedSize) {
646
+ throw new CliError(
647
+ "PeakURL core package failed ZIP size verification."
648
+ );
649
+ }
650
+ await mkdir2(dirname2(destinationPath), { recursive: true });
651
+ await writeFile2(destinationPath, content);
652
+ const mode = entry.mode & 511;
653
+ if (mode > 0) {
654
+ await chmod2(destinationPath, mode);
655
+ }
656
+ }
657
+ async function ensureNoEntryConflicts(entries, targetPath, force) {
658
+ for (const entry of entries) {
659
+ const destinationPath = join2(targetPath, entry.path);
660
+ try {
661
+ const destinationStats = await lstat(destinationPath);
662
+ if (entry.isDirectory) {
663
+ if (!destinationStats.isDirectory()) {
664
+ throw new CliError(
665
+ `Cannot extract PeakURL core files because '${entry.path}' already exists as a file.`
666
+ );
667
+ }
668
+ continue;
669
+ }
670
+ if (destinationStats.isDirectory()) {
671
+ throw new CliError(
672
+ `Cannot overwrite '${entry.path}' because it already exists as a directory.`
673
+ );
674
+ }
675
+ if (!force) {
676
+ throw new CliError(
677
+ `Cannot extract PeakURL core files because '${entry.path}' already exists. Re-run with --force to overwrite existing files.`
678
+ );
679
+ }
680
+ } catch (error) {
681
+ const code = error.code;
682
+ if (code !== "ENOENT") {
683
+ throw error;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ async function copyExtractedEntries(entries, extractedPath, targetPath) {
689
+ for (const entry of entries) {
690
+ const sourcePath = join2(extractedPath, entry.path);
691
+ const destinationPath = join2(targetPath, entry.path);
692
+ if (entry.isDirectory) {
693
+ await mkdir2(destinationPath, { recursive: true });
694
+ continue;
695
+ }
696
+ await mkdir2(dirname2(destinationPath), { recursive: true });
697
+ await copyFile(sourcePath, destinationPath);
698
+ const mode = entry.mode & 511;
699
+ if (mode > 0) {
700
+ await chmod2(destinationPath, mode);
701
+ }
702
+ }
703
+ }
704
+ function countExtractedFiles(entries) {
705
+ return entries.filter((entry) => !entry.isDirectory).length;
706
+ }
707
+ async function getCoreRelease(env) {
708
+ const releaseApiUrl = getReleaseApiUrl(env);
709
+ const response = await fetch(releaseApiUrl, {
710
+ headers: {
711
+ accept: "application/json"
712
+ },
713
+ redirect: "follow"
714
+ });
715
+ if (!response.ok) {
716
+ throw new CliError("PeakURL release metadata could not be loaded.");
717
+ }
718
+ const payload = await response.json();
719
+ const version = asString(payload.version) || "latest";
720
+ return {
721
+ version,
722
+ downloadUrl: getCorePackageUrl(env),
723
+ checksumSha256: normalizeSha256(payload.checksumSha256),
724
+ releasedAt: asString(payload.releasedAt),
725
+ releaseNotesUrl: asString(payload.releaseNotesUrl)
726
+ };
727
+ }
728
+ async function downloadCorePackage(release, targetPath, force = false) {
729
+ const response = await fetch(release.downloadUrl, {
730
+ headers: {
731
+ accept: "application/zip, application/octet-stream;q=0.9, */*;q=0.1"
732
+ },
733
+ redirect: "follow"
734
+ });
735
+ if (!response.ok) {
736
+ throw new CliError("PeakURL core package could not be downloaded.");
737
+ }
738
+ const archive = Buffer.from(await response.arrayBuffer());
739
+ const checksum = createHash("sha256").update(archive).digest("hex");
740
+ if (checksum !== release.checksumSha256) {
741
+ throw new CliError(
742
+ `Checksum verification failed for PeakURL ${release.version}.`
743
+ );
744
+ }
745
+ const entries = parseZipEntries(archive);
746
+ const tempRoot = await mkdtemp(join2(tmpdir(), "peakurl-core-"));
747
+ const extractedPath = join2(tempRoot, "extract");
748
+ const absoluteTargetPath = resolve(targetPath);
749
+ try {
750
+ await mkdir2(extractedPath, { recursive: true });
751
+ for (const entry of entries) {
752
+ await writeZipEntry(archive, entry, extractedPath);
753
+ }
754
+ await ensureNoEntryConflicts(entries, absoluteTargetPath, force);
755
+ await copyExtractedEntries(entries, extractedPath, absoluteTargetPath);
756
+ return {
757
+ version: release.version,
758
+ path: absoluteTargetPath,
759
+ downloadUrl: release.downloadUrl,
760
+ checksumSha256: release.checksumSha256,
761
+ checksumVerified: true,
762
+ fileCount: countExtractedFiles(entries)
763
+ };
764
+ } finally {
765
+ await rm(tempRoot, { recursive: true, force: true });
766
+ }
767
+ }
768
+ function formatCoreDownload(result) {
769
+ return formatDetailsTable(
770
+ [
771
+ ["Version", result.version],
772
+ ["Path", result.path],
773
+ ["Checksum", "Verified (SHA-256)"],
774
+ ["Files", String(result.fileCount)],
775
+ ["Source", result.downloadUrl]
776
+ ],
777
+ "stdout"
778
+ );
779
+ }
780
+
781
+ // src/lib/exports.ts
782
+ var EXPORT_HEADERS = [
783
+ "url",
784
+ "alias",
785
+ "title",
786
+ "password",
787
+ "expires",
788
+ "short_url",
789
+ "clicks",
790
+ "unique_clicks",
791
+ "created_at"
792
+ ];
793
+ function text(value) {
794
+ return typeof value === "string" ? value : "";
795
+ }
796
+ function csvValue(value) {
797
+ const content = value == null ? "" : String(value);
798
+ if (/[",\r\n]/.test(content)) {
799
+ return `"${content.replace(/"/g, '""')}"`;
800
+ }
801
+ return content;
802
+ }
803
+ function xmlValue(value) {
804
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
805
+ }
806
+ function aliasValue(link) {
807
+ return text(link.alias) || text(link.shortCode);
808
+ }
809
+ function parseFileFormat(value) {
810
+ const format = value.trim().toLowerCase();
811
+ if (format === "csv" || format === "json" || format === "xml") {
812
+ return format;
813
+ }
814
+ throw new CliError(
815
+ `Unsupported file format: ${value}. Use csv, json, or xml.`
816
+ );
817
+ }
818
+ function getFileFormatFromPath(filePath) {
819
+ const value = filePath.trim().toLowerCase();
820
+ if (value.endsWith(".csv")) {
821
+ return "csv";
822
+ }
823
+ if (value.endsWith(".json")) {
824
+ return "json";
825
+ }
826
+ if (value.endsWith(".xml")) {
827
+ return "xml";
828
+ }
829
+ return void 0;
830
+ }
831
+ function getExportFileName(format) {
832
+ return `peakurl-links.${format}`;
833
+ }
834
+ function buildExportRows(links) {
835
+ return links.map((link) => ({
836
+ url: text(link.destinationUrl),
837
+ alias: aliasValue(link),
838
+ title: text(link.title),
839
+ password: "",
840
+ expires: text(link.expiresAt),
841
+ short_url: text(link.shortUrl),
842
+ clicks: typeof link.clicks === "number" ? link.clicks : "",
843
+ unique_clicks: typeof link.uniqueClicks === "number" ? link.uniqueClicks : "",
844
+ created_at: text(link.createdAt)
845
+ }));
846
+ }
847
+ function serializeLinkExport(links, format) {
848
+ const rows2 = buildExportRows(links);
849
+ if (format === "json") {
850
+ return JSON.stringify(rows2, null, 2);
851
+ }
852
+ if (format === "xml") {
853
+ const body = rows2.map(
854
+ (row2) => ` <url>
855
+ <destinationUrl>${xmlValue(row2.url)}</destinationUrl>
856
+ <alias>${xmlValue(row2.alias)}</alias>
857
+ <title>${xmlValue(row2.title)}</title>
858
+ <password>${xmlValue(row2.password)}</password>
859
+ <expiresAt>${xmlValue(row2.expires)}</expiresAt>
860
+ <shortUrl>${xmlValue(row2.short_url)}</shortUrl>
861
+ <clicks>${xmlValue(row2.clicks)}</clicks>
862
+ <uniqueClicks>${xmlValue(row2.unique_clicks)}</uniqueClicks>
863
+ <createdAt>${xmlValue(row2.created_at)}</createdAt>
864
+ </url>`
865
+ ).join("\n");
866
+ return `<urls>
867
+ ${body}
868
+ </urls>
869
+ `;
870
+ }
871
+ const lines = [
872
+ EXPORT_HEADERS.join(","),
873
+ ...rows2.map(
874
+ (row2) => EXPORT_HEADERS.map((key) => csvValue(row2[key])).join(",")
875
+ )
876
+ ];
877
+ return `${lines.join("\n")}
878
+ `;
742
879
  }
743
880
 
744
881
  // src/lib/imports.ts
882
+ import { readFile as readFile2 } from "fs/promises";
745
883
  function text2(value) {
746
884
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
747
885
  }
@@ -1001,7 +1139,7 @@ var LIST_KEYS = ["urls", "items", "results"];
1001
1139
  function asObject(value) {
1002
1140
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1003
1141
  }
1004
- function asString(value) {
1142
+ function asString2(value) {
1005
1143
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1006
1144
  }
1007
1145
  function asNumber(value) {
@@ -1009,7 +1147,7 @@ function asNumber(value) {
1009
1147
  }
1010
1148
  function pickText(link, keys) {
1011
1149
  for (const key of keys) {
1012
- const value = asString(link[key]);
1150
+ const value = asString2(link[key]);
1013
1151
  if (value) {
1014
1152
  return value;
1015
1153
  }
@@ -1076,21 +1214,24 @@ function getQuietLinkValue(link) {
1076
1214
  return getLinkShortUrl(link) || getLinkAlias(link) || getLinkId(link) || "";
1077
1215
  }
1078
1216
  function formatLinkDetails(link) {
1079
- const lines = [
1217
+ const rows2 = [
1080
1218
  ["ID", getLinkId(link)],
1081
1219
  ["Alias", getLinkAlias(link)],
1082
1220
  ["Short URL", getLinkShortUrl(link)],
1083
1221
  ["Destination", getLinkDestination(link)],
1084
- ["Title", asString(link.title)],
1085
- ["Status", asString(link.status)],
1222
+ ["Title", asString2(link.title)],
1223
+ ["Status", asString2(link.status)],
1086
1224
  [
1087
1225
  "Clicks",
1088
1226
  asNumber(link.clicks) === void 0 ? void 0 : String(link.clicks)
1089
1227
  ],
1090
- ["Created", asString(link.createdAt)],
1091
- ["Updated", asString(link.updatedAt)]
1228
+ ["Created", asString2(link.createdAt)],
1229
+ ["Updated", asString2(link.updatedAt)]
1092
1230
  ].filter((entry) => Boolean(entry[1]));
1093
- return lines.map(([label, value]) => `${label}: ${value}`).join("\n");
1231
+ if (rows2.length === 0) {
1232
+ return "No link fields returned.";
1233
+ }
1234
+ return formatDetailsTable(rows2);
1094
1235
  }
1095
1236
  function formatLinksTable(links) {
1096
1237
  if (links.length === 0) {
@@ -1102,7 +1243,7 @@ function formatLinksTable(links) {
1102
1243
  truncate(getLinkAlias(link) || "-", 12),
1103
1244
  truncate(getLinkShortUrl(link) || "-", 36),
1104
1245
  truncate(getLinkDestination(link) || "-", 52),
1105
- truncate(asString(link.status) || "-", 12)
1246
+ truncate(asString2(link.status) || "-", 12)
1106
1247
  ]);
1107
1248
  return formatTable(headers, rows2);
1108
1249
  }
@@ -1415,7 +1556,7 @@ function section(title, rows2) {
1415
1556
  return void 0;
1416
1557
  }
1417
1558
  return `${title}
1418
- ${formatTable(["Field", "Value"], rows2)}`;
1559
+ ${formatDetailsTable(rows2)}`;
1419
1560
  }
1420
1561
  function getStatusValue(status2) {
1421
1562
  return text3(getSummary(status2).overall) || "unknown";
@@ -1668,7 +1809,7 @@ function userTable(user) {
1668
1809
  if (rows2.length === 0) {
1669
1810
  return "No user fields returned.";
1670
1811
  }
1671
- return formatTable(["Field", "Value"], rows2);
1812
+ return formatDetailsTable(rows2);
1672
1813
  }
1673
1814
 
1674
1815
  // src/lib/webhooks.ts
@@ -1771,12 +1912,254 @@ function formatWebhookDetails(webhook) {
1771
1912
  if (rows2.length === 0) {
1772
1913
  return "No webhook fields returned.";
1773
1914
  }
1774
- return formatTable(["Field", "Value"], rows2);
1915
+ return formatDetailsTable(rows2);
1775
1916
  }
1776
1917
  function formatWebhooksSummary(webhooks) {
1777
1918
  return `${webhooks.length} webhook${webhooks.length === 1 ? "" : "s"} returned.`;
1778
1919
  }
1779
1920
 
1921
+ // src/commands/core.ts
1922
+ async function downloadCore(options) {
1923
+ const release = await getCoreRelease(process.env);
1924
+ const result = await downloadCorePackage(
1925
+ release,
1926
+ cwd(),
1927
+ Boolean(options.force)
1928
+ );
1929
+ const responseBody = {
1930
+ success: true,
1931
+ message: "PeakURL downloaded.",
1932
+ data: result,
1933
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1934
+ };
1935
+ if (options.json) {
1936
+ writeJson(responseBody);
1937
+ return;
1938
+ }
1939
+ if (options.quiet) {
1940
+ writeStdout(result.path);
1941
+ return;
1942
+ }
1943
+ writeStdout(successLine(responseBody.message));
1944
+ writeStdout(formatCoreDownload(result));
1945
+ }
1946
+
1947
+ // src/commands/links.ts
1948
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1949
+ import { dirname as dirname3, resolve as resolve2 } from "path";
1950
+
1951
+ // src/api/client.ts
1952
+ function isApiResponse(value) {
1953
+ return Boolean(
1954
+ value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
1955
+ );
1956
+ }
1957
+ function networkError(apiBaseUrl, error) {
1958
+ if (error instanceof Error && error.message) {
1959
+ return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
1960
+ }
1961
+ return `Could not reach PeakURL at ${apiBaseUrl}.`;
1962
+ }
1963
+ var ApiClient = class {
1964
+ /**
1965
+ * Creates a client bound to one resolved credential set.
1966
+ *
1967
+ * @param config Explicit API base URL plus bearer API key.
1968
+ */
1969
+ constructor(config) {
1970
+ this.config = config;
1971
+ }
1972
+ config;
1973
+ /**
1974
+ * Loads the currently authenticated user.
1975
+ *
1976
+ * PeakURL accepts bearer API keys on `GET /users/me`, which is also the
1977
+ * CLI login verification flow.
1978
+ *
1979
+ * @returns API response envelope containing the authenticated user.
1980
+ */
1981
+ whoami() {
1982
+ return this.request("GET", "users/me");
1983
+ }
1984
+ /**
1985
+ * Loads the current system status snapshot for the authenticated site.
1986
+ *
1987
+ * @returns API response envelope containing system status sections.
1988
+ */
1989
+ getStatus() {
1990
+ return this.request("GET", "system/status");
1991
+ }
1992
+ /**
1993
+ * Creates a short URL.
1994
+ *
1995
+ * @param payload Request body accepted by `POST /api/v1/urls`.
1996
+ * @returns API response envelope containing the created link.
1997
+ */
1998
+ createUrl(payload) {
1999
+ return this.request("POST", "urls", payload);
2000
+ }
2001
+ /**
2002
+ * Lists short URLs with optional pagination and filtering.
2003
+ *
2004
+ * The current PeakURL app returns `{ items, meta }` under `data`, but the
2005
+ * CLI keeps a slightly broader compatibility type for future-proofing.
2006
+ *
2007
+ * @param query Optional query-string values.
2008
+ * @returns API response envelope containing list data.
2009
+ */
2010
+ listUrls(query) {
2011
+ return this.request("GET", "urls", void 0, query);
2012
+ }
2013
+ /**
2014
+ * Exports the full accessible link dataset for the authenticated user.
2015
+ *
2016
+ * @param query Optional search and sort values.
2017
+ * @returns API response envelope containing the full export payload.
2018
+ */
2019
+ exportUrls(query) {
2020
+ return this.request(
2021
+ "GET",
2022
+ "urls/export",
2023
+ void 0,
2024
+ query
2025
+ );
2026
+ }
2027
+ /**
2028
+ * Imports multiple short links in one bulk request.
2029
+ *
2030
+ * @param payload Request body accepted by `POST /api/v1/urls/bulk`.
2031
+ * @returns API response envelope containing created rows plus row errors.
2032
+ */
2033
+ importUrls(payload) {
2034
+ return this.request("POST", "urls/bulk", payload);
2035
+ }
2036
+ /**
2037
+ * Loads a single short URL by identifier or alias.
2038
+ *
2039
+ * PeakURL resolves IDs, short codes, and aliases through the same route.
2040
+ *
2041
+ * @param idOrAlias Link identifier, short code, or alias.
2042
+ * @returns API response envelope containing the resolved link.
2043
+ */
2044
+ getUrl(idOrAlias) {
2045
+ return this.request(
2046
+ "GET",
2047
+ `urls/${encodeURIComponent(idOrAlias)}`
2048
+ );
2049
+ }
2050
+ /**
2051
+ * Deletes a short URL by its stable row ID.
2052
+ *
2053
+ * The current PeakURL backend delete route expects the row ID. The CLI can
2054
+ * still accept an alias at the command layer by resolving it first.
2055
+ *
2056
+ * @param id Stable link row ID.
2057
+ * @returns API response envelope containing the deletion result.
2058
+ */
2059
+ deleteUrl(id) {
2060
+ return this.request(
2061
+ "DELETE",
2062
+ `urls/${encodeURIComponent(id)}`
2063
+ );
2064
+ }
2065
+ /**
2066
+ * Lists outbound webhooks for the authenticated user.
2067
+ *
2068
+ * @returns API response envelope containing webhook rows.
2069
+ */
2070
+ listWebhooks() {
2071
+ return this.request("GET", "webhooks");
2072
+ }
2073
+ /**
2074
+ * Creates one outbound webhook subscription.
2075
+ *
2076
+ * @param payload Request body accepted by `POST /api/v1/webhooks`.
2077
+ * @returns API response envelope containing the created webhook.
2078
+ */
2079
+ createWebhook(payload) {
2080
+ return this.request("POST", "webhooks", payload);
2081
+ }
2082
+ /**
2083
+ * Deletes one webhook by its stable row ID.
2084
+ *
2085
+ * @param id Webhook identifier returned by the list/create endpoints.
2086
+ * @returns API response envelope containing the deletion result.
2087
+ */
2088
+ deleteWebhook(id) {
2089
+ return this.request(
2090
+ "DELETE",
2091
+ `webhooks/${encodeURIComponent(id)}`
2092
+ );
2093
+ }
2094
+ /**
2095
+ * Performs one authenticated API request and normalizes the response.
2096
+ *
2097
+ * @param method HTTP method to send.
2098
+ * @param path Route path relative to `/api/v1`.
2099
+ * @param body Optional JSON body.
2100
+ * @param query Optional query-string values.
2101
+ * @returns Parsed PeakURL response envelope.
2102
+ * @throws {CliError} When the network request fails or the API returns an error.
2103
+ */
2104
+ async request(method, path, body, query) {
2105
+ const url = buildApiUrl(this.config.apiBaseUrl, path, query);
2106
+ let response;
2107
+ try {
2108
+ response = await fetch(url, {
2109
+ method,
2110
+ headers: {
2111
+ Accept: "application/json",
2112
+ Authorization: `Bearer ${this.config.apiKey}`,
2113
+ ...body ? { "Content-Type": "application/json" } : {}
2114
+ },
2115
+ body: body ? JSON.stringify(body) : void 0
2116
+ });
2117
+ } catch (error) {
2118
+ throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
2119
+ cause: error instanceof Error ? error : void 0
2120
+ });
2121
+ }
2122
+ const rawText = await response.text();
2123
+ if (!rawText) {
2124
+ if (!response.ok) {
2125
+ throw new CliError(
2126
+ `PeakURL request failed with HTTP ${response.status}.`
2127
+ );
2128
+ }
2129
+ return {
2130
+ success: true,
2131
+ message: "Request completed.",
2132
+ data: void 0,
2133
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2134
+ };
2135
+ }
2136
+ let parsed;
2137
+ try {
2138
+ parsed = JSON.parse(rawText);
2139
+ } catch {
2140
+ if (!response.ok) {
2141
+ throw new CliError(
2142
+ `PeakURL request failed with HTTP ${response.status}.`
2143
+ );
2144
+ }
2145
+ throw new CliError("PeakURL returned an invalid JSON response.");
2146
+ }
2147
+ if (!isApiResponse(parsed)) {
2148
+ throw new CliError(
2149
+ "PeakURL returned an unexpected response envelope."
2150
+ );
2151
+ }
2152
+ if (!response.ok || !parsed.success) {
2153
+ const statusCode = response.status === 401 ? 2 : 1;
2154
+ throw new CliError(
2155
+ parsed.message || `PeakURL request failed with HTTP ${response.status}.`,
2156
+ statusCode
2157
+ );
2158
+ }
2159
+ return parsed;
2160
+ }
2161
+ };
2162
+
1780
2163
  // src/commands/links.ts
1781
2164
  function normalizeExpiresAt(value) {
1782
2165
  if (!value) {
@@ -1810,7 +2193,7 @@ async function createLink(destinationUrl, options) {
1810
2193
  writeStdout(getQuietLinkValue(response.data));
1811
2194
  return;
1812
2195
  }
1813
- writeStdout(response.message);
2196
+ writeStdout(successLine(response.message));
1814
2197
  writeStdout(formatLinkDetails(response.data));
1815
2198
  }
1816
2199
  async function importLinks(filePath, options) {
@@ -1833,7 +2216,7 @@ async function importLinks(filePath, options) {
1833
2216
  }
1834
2217
  return;
1835
2218
  }
1836
- writeStdout(response.message);
2219
+ writeStdout(successLine(response.message));
1837
2220
  if (links.length > 0) {
1838
2221
  writeStdout(formatLinksTable(links));
1839
2222
  } else {
@@ -1861,10 +2244,10 @@ async function exportLinks(options) {
1861
2244
  process.stdout.write(content);
1862
2245
  return;
1863
2246
  }
1864
- const filePath = resolve(options.output || getExportFileName(format));
2247
+ const filePath = resolve2(options.output || getExportFileName(format));
1865
2248
  try {
1866
- await mkdir2(dirname2(filePath), { recursive: true });
1867
- await writeFile2(filePath, content, "utf8");
2249
+ await mkdir3(dirname3(filePath), { recursive: true });
2250
+ await writeFile3(filePath, content, "utf8");
1868
2251
  } catch (error) {
1869
2252
  throw new CliError(`Could not write export file ${filePath}.`, 1, {
1870
2253
  cause: error instanceof Error ? error : void 0
@@ -1874,7 +2257,7 @@ async function exportLinks(options) {
1874
2257
  writeStdout(filePath);
1875
2258
  return;
1876
2259
  }
1877
- writeStdout(response.message);
2260
+ writeStdout(successLine(response.message));
1878
2261
  writeStdout(
1879
2262
  `Saved ${links.length} link${links.length === 1 ? "" : "s"} to ${filePath}.`
1880
2263
  );
@@ -1902,7 +2285,7 @@ async function listLinks(options) {
1902
2285
  }
1903
2286
  return;
1904
2287
  }
1905
- writeStdout(response.message);
2288
+ writeStdout(successLine(response.message));
1906
2289
  writeStdout(formatLinksTable(links));
1907
2290
  writeStdout(formatListSummary(response.data, links.length));
1908
2291
  }
@@ -1917,7 +2300,7 @@ async function getLink(idOrAlias, options) {
1917
2300
  writeStdout(getQuietLinkValue(response.data));
1918
2301
  return;
1919
2302
  }
1920
- writeStdout(response.message);
2303
+ writeStdout(successLine(response.message));
1921
2304
  writeStdout(formatLinkDetails(response.data));
1922
2305
  }
1923
2306
  async function deleteLink(idOrAlias, options) {
@@ -1938,7 +2321,7 @@ async function deleteLink(idOrAlias, options) {
1938
2321
  if (options.quiet) {
1939
2322
  return;
1940
2323
  }
1941
- writeStdout(response.message);
2324
+ writeStdout(successLine(response.message));
1942
2325
  }
1943
2326
 
1944
2327
  // src/commands/login.ts
@@ -1963,7 +2346,9 @@ async function login(options) {
1963
2346
  if (options.quiet) {
1964
2347
  return;
1965
2348
  }
1966
- writeStdout(`Saved credentials for ${credentials.apiBaseUrl}`);
2349
+ writeStdout(
2350
+ successLine(`Saved credentials for ${credentials.apiBaseUrl}.`)
2351
+ );
1967
2352
  writeStdout(`Authenticated as ${userLabel(response.data)}`);
1968
2353
  writeStdout(userTable(response.data));
1969
2354
  }
@@ -1995,7 +2380,7 @@ async function logout(options) {
1995
2380
  if (options.quiet) {
1996
2381
  return;
1997
2382
  }
1998
- writeStdout(message);
2383
+ writeStdout(successLine(message));
1999
2384
  if (envConfig) {
2000
2385
  writeStdout(
2001
2386
  "Environment credentials in PEAKURL_BASE_URL or PEAKURL_API_KEY still apply in this shell."
@@ -2015,7 +2400,7 @@ async function status(options) {
2015
2400
  writeStdout(getStatusValue(response.data));
2016
2401
  return;
2017
2402
  }
2018
- writeStdout(response.message);
2403
+ writeStdout(successLine(response.message));
2019
2404
  writeStdout(formatStatusReport(response.data));
2020
2405
  }
2021
2406
 
@@ -2042,7 +2427,11 @@ async function checkUpdate(options, currentVersion) {
2042
2427
  }
2043
2428
  if (!status2.isOutdated) {
2044
2429
  if (!options.quiet) {
2045
- writeStdout(`PeakURL CLI ${status2.currentVersion} is up to date.`);
2430
+ writeStdout(
2431
+ successLine(
2432
+ `PeakURL CLI ${status2.currentVersion} is up to date.`
2433
+ )
2434
+ );
2046
2435
  }
2047
2436
  return;
2048
2437
  }
@@ -2077,7 +2466,7 @@ async function listWebhooks(options) {
2077
2466
  }
2078
2467
  return;
2079
2468
  }
2080
- writeStdout(response.message);
2469
+ writeStdout(successLine(response.message));
2081
2470
  writeStdout(formatWebhooksTable(response.data ?? []));
2082
2471
  writeStdout(formatWebhooksSummary(response.data ?? []));
2083
2472
  }
@@ -2101,7 +2490,7 @@ async function createWebhook(url, options) {
2101
2490
  writeStdout(getQuietWebhookValue(response.data));
2102
2491
  return;
2103
2492
  }
2104
- writeStdout(response.message);
2493
+ writeStdout(successLine(response.message));
2105
2494
  writeStdout(formatWebhookDetails(response.data));
2106
2495
  if (response.data.secret) {
2107
2496
  writeStdout("Save the signing secret now. PeakURL only shows it once.");
@@ -2117,7 +2506,7 @@ async function deleteWebhook(id, options) {
2117
2506
  if (options.quiet) {
2118
2507
  return;
2119
2508
  }
2120
- writeStdout(response.message);
2509
+ writeStdout(successLine(response.message));
2121
2510
  }
2122
2511
  async function listWebhookEvents(options) {
2123
2512
  const response = {
@@ -2136,7 +2525,7 @@ async function listWebhookEvents(options) {
2136
2525
  }
2137
2526
  return;
2138
2527
  }
2139
- writeStdout(response.message);
2528
+ writeStdout(successLine(response.message));
2140
2529
  writeStdout(formatWebhookEventsTable());
2141
2530
  }
2142
2531
 
@@ -2152,7 +2541,7 @@ async function whoami(options) {
2152
2541
  writeStdout(userValue(response.data));
2153
2542
  return;
2154
2543
  }
2155
- writeStdout(response.message);
2544
+ writeStdout(successLine(response.message));
2156
2545
  writeStdout(userTable(response.data));
2157
2546
  }
2158
2547
 
@@ -2211,6 +2600,7 @@ Get Started:
2211
2600
 
2212
2601
  Common Commands:
2213
2602
  peakurl status
2603
+ peakurl core download
2214
2604
  peakurl list --limit 10
2215
2605
  peakurl import ./links.csv
2216
2606
  peakurl export --format csv
@@ -2255,6 +2645,17 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
2255
2645
  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
2646
  ["peakurl status", "peakurl status --json", "peakurl status --quiet"]
2257
2647
  );
2648
+ const core = program.command("core").summary("Manage PeakURL core files").helpOption("-h, --help", "Show help").description("Manage PeakURL core package downloads.");
2649
+ addExamples(core, [
2650
+ "peakurl core download",
2651
+ "peakurl core download --force"
2652
+ ]);
2653
+ addExamples(
2654
+ core.command("download").summary("Download the core package").description(
2655
+ "Download the latest PeakURL core package, verify its checksum, and extract it into the current directory."
2656
+ ).helpOption("-h, --help", "Show help").option("--force", "Overwrite existing files when needed").option("--json", "Print machine-readable output").option("--quiet", "Print only the extracted path").action(downloadCore),
2657
+ ["peakurl core download", "peakurl core download --force --json"]
2658
+ );
2258
2659
  addExamples(
2259
2660
  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
2661
  "--status <status>",
@@ -2354,7 +2755,7 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
2354
2755
  const cliError = ensureCliError(error);
2355
2756
  if (cliError.kind === "auth_required") {
2356
2757
  const commandName = getRetryCommandName(process.argv);
2357
- writeStderr("Authentication required.");
2758
+ writeStderr(errorLine("Authentication required."));
2358
2759
  writeStderr("PeakURL could not find credentials for this command.");
2359
2760
  writeStderr(
2360
2761
  "Use one of the first two steps below, then run the last command."
@@ -2367,7 +2768,7 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
2367
2768
  )
2368
2769
  );
2369
2770
  } else {
2370
- writeStderr(cliError.message);
2771
+ writeStderr(errorLine(cliError.message));
2371
2772
  }
2372
2773
  process.exit(cliError.exitCode);
2373
2774
  }