peakurl 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/peakurl.js CHANGED
@@ -4,14 +4,6 @@
4
4
  import { readFile as readFile3 } from "fs/promises";
5
5
  import { Command, CommanderError, InvalidArgumentError } from "commander";
6
6
 
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";
14
-
15
7
  // src/lib/errors.ts
16
8
  var CliError = class extends Error {
17
9
  exitCode;
@@ -27,13 +19,682 @@ function ensureCliError(error) {
27
19
  if (error instanceof CliError) {
28
20
  return error;
29
21
  }
30
- if (error instanceof Error) {
31
- return new CliError(error.message, 1, { cause: error });
22
+ if (error instanceof Error) {
23
+ return new CliError(error.message, 1, { cause: error });
24
+ }
25
+ return new CliError("Unexpected error.");
26
+ }
27
+
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
+ }
37
+ function getApiBaseUrl(value) {
38
+ const input = value.trim();
39
+ if (!input) {
40
+ throw new CliError("A PeakURL API base URL is required.");
41
+ }
42
+ let parsed;
43
+ try {
44
+ parsed = new URL(input);
45
+ } catch {
46
+ throw new CliError(`Invalid API base URL: ${value}`);
47
+ }
48
+ validateHttpUrl(parsed, "PeakURL API base URL");
49
+ parsed.hash = "";
50
+ parsed.search = "";
51
+ const pathname = parsed.pathname.replace(/\/+$/, "");
52
+ if (!/\/api\/v1$/i.test(pathname)) {
53
+ throw new CliError("PeakURL API base URL must end with /api/v1.");
54
+ }
55
+ return `${parsed.origin}${pathname}`;
56
+ }
57
+ function buildApiUrl(apiBaseUrl, path, query) {
58
+ const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
59
+ const cleanPath = path.replace(/^\/+/, "");
60
+ const url = new URL(cleanPath, `${cleanBaseUrl}/`);
61
+ for (const [key, value] of Object.entries(query ?? {})) {
62
+ if (value === void 0 || value === "") {
63
+ continue;
64
+ }
65
+ url.searchParams.set(key, String(value));
66
+ }
67
+ return url.toString();
68
+ }
69
+ function normalizeDestinationUrl(value) {
70
+ const input = value.trim();
71
+ if (!input) {
72
+ throw new CliError("A destination URL is required.");
73
+ }
74
+ try {
75
+ const parsed = new URL(input);
76
+ validateHttpUrl(parsed, "Destination URL");
77
+ return parsed.toString();
78
+ } catch (error) {
79
+ if (error instanceof CliError) {
80
+ throw error;
81
+ }
82
+ throw new CliError(`Invalid destination URL: ${value}`);
83
+ }
84
+ }
85
+ function normalizeWebhookUrl(value) {
86
+ const input = value.trim();
87
+ if (!input) {
88
+ throw new CliError("A webhook URL is required.");
89
+ }
90
+ try {
91
+ const parsed = new URL(input);
92
+ validateHttpUrl(parsed, "Webhook URL");
93
+ return parsed.toString();
94
+ } catch (error) {
95
+ if (error instanceof CliError) {
96
+ throw error;
97
+ }
98
+ throw new CliError(`Invalid webhook URL: ${value}`);
99
+ }
100
+ }
101
+
102
+ // src/api/client.ts
103
+ function isApiResponse(value) {
104
+ return Boolean(
105
+ value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
106
+ );
107
+ }
108
+ function networkError(apiBaseUrl, error) {
109
+ if (error instanceof Error && error.message) {
110
+ return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
111
+ }
112
+ return `Could not reach PeakURL at ${apiBaseUrl}.`;
113
+ }
114
+ var ApiClient = class {
115
+ /**
116
+ * Creates a client bound to one resolved credential set.
117
+ *
118
+ * @param config Explicit API base URL plus bearer API key.
119
+ */
120
+ constructor(config) {
121
+ this.config = config;
122
+ }
123
+ /**
124
+ * Loads the currently authenticated user.
125
+ *
126
+ * PeakURL accepts bearer API keys on `GET /users/me`, which is also the
127
+ * CLI login verification flow.
128
+ *
129
+ * @returns API response envelope containing the authenticated user.
130
+ */
131
+ whoami() {
132
+ return this.request("GET", "users/me");
133
+ }
134
+ /**
135
+ * Loads the current system status snapshot for the authenticated site.
136
+ *
137
+ * @returns API response envelope containing system status sections.
138
+ */
139
+ getStatus() {
140
+ return this.request("GET", "system/status");
141
+ }
142
+ /**
143
+ * Creates a short URL.
144
+ *
145
+ * @param payload Request body accepted by `POST /api/v1/urls`.
146
+ * @returns API response envelope containing the created link.
147
+ */
148
+ createUrl(payload) {
149
+ return this.request("POST", "urls", payload);
150
+ }
151
+ /**
152
+ * Lists short URLs with optional pagination and filtering.
153
+ *
154
+ * The current PeakURL app returns `{ items, meta }` under `data`, but the
155
+ * CLI keeps a slightly broader compatibility type for future-proofing.
156
+ *
157
+ * @param query Optional query-string values.
158
+ * @returns API response envelope containing list data.
159
+ */
160
+ listUrls(query) {
161
+ return this.request("GET", "urls", void 0, query);
162
+ }
163
+ /**
164
+ * Exports the full accessible link dataset for the authenticated user.
165
+ *
166
+ * @param query Optional search and sort values.
167
+ * @returns API response envelope containing the full export payload.
168
+ */
169
+ exportUrls(query) {
170
+ return this.request(
171
+ "GET",
172
+ "urls/export",
173
+ void 0,
174
+ query
175
+ );
176
+ }
177
+ /**
178
+ * Imports multiple short links in one bulk request.
179
+ *
180
+ * @param payload Request body accepted by `POST /api/v1/urls/bulk`.
181
+ * @returns API response envelope containing created rows plus row errors.
182
+ */
183
+ importUrls(payload) {
184
+ return this.request("POST", "urls/bulk", payload);
185
+ }
186
+ /**
187
+ * Loads a single short URL by identifier or alias.
188
+ *
189
+ * PeakURL resolves IDs, short codes, and aliases through the same route.
190
+ *
191
+ * @param idOrAlias Link identifier, short code, or alias.
192
+ * @returns API response envelope containing the resolved link.
193
+ */
194
+ getUrl(idOrAlias) {
195
+ return this.request(
196
+ "GET",
197
+ `urls/${encodeURIComponent(idOrAlias)}`
198
+ );
199
+ }
200
+ /**
201
+ * Deletes a short URL by its stable row ID.
202
+ *
203
+ * The current PeakURL backend delete route expects the row ID. The CLI can
204
+ * still accept an alias at the command layer by resolving it first.
205
+ *
206
+ * @param id Stable link row ID.
207
+ * @returns API response envelope containing the deletion result.
208
+ */
209
+ deleteUrl(id) {
210
+ return this.request(
211
+ "DELETE",
212
+ `urls/${encodeURIComponent(id)}`
213
+ );
214
+ }
215
+ /**
216
+ * Deletes multiple short URLs in a single bulk operation.
217
+ *
218
+ * @param ids Array of short URL IDs to delete.
219
+ * @returns API response envelope with deleted count.
220
+ */
221
+ deleteUrlsBulk(ids) {
222
+ return this.request("DELETE", "urls/bulk", { ids });
223
+ }
224
+ /**
225
+ * Deletes all accessible short URLs for the authenticated user.
226
+ *
227
+ * @returns API response envelope with deleted count.
228
+ */
229
+ clearUrls() {
230
+ return this.request("DELETE", "urls");
231
+ }
232
+ /**
233
+ * Empties all short links currently in trash.
234
+ *
235
+ * @returns API response envelope with deleted count.
236
+ */
237
+ emptyTrash() {
238
+ return this.request("DELETE", "urls/trash");
239
+ }
240
+ /**
241
+ * Lists audit log activity entries.
242
+ *
243
+ * @param query Optional query-string parameters for pagination or filters.
244
+ * @returns API response envelope containing activity items and meta.
245
+ */
246
+ listActivity(query) {
247
+ return this.request(
248
+ "GET",
249
+ "analytics/activity",
250
+ void 0,
251
+ query
252
+ );
253
+ }
254
+ /**
255
+ * Deletes a single audit log activity entry by its row ID.
256
+ *
257
+ * @param id Audit log row ID.
258
+ * @returns API response envelope confirming deletion.
259
+ */
260
+ deleteActivity(id) {
261
+ return this.request(
262
+ "DELETE",
263
+ `analytics/activity/${encodeURIComponent(id)}`
264
+ );
265
+ }
266
+ /**
267
+ * Deletes multiple audit log activity entries in bulk.
268
+ *
269
+ * @param ids Array of audit log row IDs.
270
+ * @returns API response envelope with deleted count.
271
+ */
272
+ deleteActivityBulk(ids) {
273
+ return this.request(
274
+ "DELETE",
275
+ "analytics/activity/bulk",
276
+ { ids }
277
+ );
278
+ }
279
+ /**
280
+ * Clears all audit log activity records.
281
+ *
282
+ * @returns API response envelope confirming all logs were deleted.
283
+ */
284
+ clearActivity() {
285
+ return this.request("DELETE", "analytics/activity");
286
+ }
287
+ /**
288
+ * Lists outbound webhooks for the authenticated user.
289
+ *
290
+ * @returns API response envelope containing webhook rows.
291
+ */
292
+ listWebhooks() {
293
+ return this.request("GET", "webhooks");
294
+ }
295
+ /**
296
+ * Creates one outbound webhook subscription.
297
+ *
298
+ * @param payload Request body accepted by `POST /api/v1/webhooks`.
299
+ * @returns API response envelope containing the created webhook.
300
+ */
301
+ createWebhook(payload) {
302
+ return this.request("POST", "webhooks", payload);
303
+ }
304
+ /**
305
+ * Deletes one webhook by its stable row ID.
306
+ *
307
+ * @param id Webhook identifier returned by the list/create endpoints.
308
+ * @returns API response envelope containing the deletion result.
309
+ */
310
+ deleteWebhook(id) {
311
+ return this.request(
312
+ "DELETE",
313
+ `webhooks/${encodeURIComponent(id)}`
314
+ );
315
+ }
316
+ /**
317
+ * Loads the current status of the cron scheduler.
318
+ *
319
+ * @returns API response envelope containing the scheduler status.
320
+ */
321
+ getJobStatus() {
322
+ return this.request("GET", "system/cron");
323
+ }
324
+ /**
325
+ * Triggers all due cron jobs to run.
326
+ *
327
+ * @returns API response envelope containing the execution results.
328
+ */
329
+ runDueJobs() {
330
+ return this.request("POST", "system/cron/run");
331
+ }
332
+ /**
333
+ * Forces a specific cron job to run immediately.
334
+ *
335
+ * @param id Job identifier.
336
+ * @returns API response envelope containing the execution result.
337
+ */
338
+ runJob(id) {
339
+ return this.request(
340
+ "POST",
341
+ `system/cron/run/${encodeURIComponent(id)}`
342
+ );
343
+ }
344
+ /**
345
+ * Clears cron execution history.
346
+ *
347
+ * @param jobId Optional job identifier to clear history only for one job.
348
+ * @returns API response envelope containing the deleted count.
349
+ */
350
+ clearJobHistory(jobId) {
351
+ return this.request(
352
+ "POST",
353
+ "system/cron/history/clear",
354
+ jobId ? { job_id: jobId } : void 0
355
+ );
356
+ }
357
+ /**
358
+ * Updates the schedule configuration for a cron job.
359
+ *
360
+ * @param id Job identifier.
361
+ * @param payload New configuration options.
362
+ * @returns API response envelope containing the updated job.
363
+ */
364
+ updateJobSchedule(id, payload) {
365
+ return this.request(
366
+ "PATCH",
367
+ `system/cron/jobs/${encodeURIComponent(id)}`,
368
+ payload
369
+ );
370
+ }
371
+ /**
372
+ * Resets a cron job schedule to its default configuration.
373
+ *
374
+ * @param id Job identifier.
375
+ * @returns API response envelope containing the restored job.
376
+ */
377
+ resetJobSchedule(id) {
378
+ return this.request(
379
+ "POST",
380
+ `system/cron/jobs/${encodeURIComponent(id)}/reset`
381
+ );
382
+ }
383
+ /**
384
+ * Updates the global cron settings, such as history retention.
385
+ *
386
+ * @param payload New global settings.
387
+ * @returns API response envelope containing the updated retention settings.
388
+ */
389
+ updateJobSettings(payload) {
390
+ return this.request(
391
+ "POST",
392
+ "system/cron/settings",
393
+ payload
394
+ );
395
+ }
396
+ /**
397
+ * Performs one authenticated API request and normalizes the response.
398
+ *
399
+ * @param method HTTP method to send.
400
+ * @param path Route path relative to `/api/v1`.
401
+ * @param body Optional JSON body.
402
+ * @param query Optional query-string values.
403
+ * @returns Parsed PeakURL response envelope.
404
+ * @throws {CliError} When the network request fails or the API returns an error.
405
+ */
406
+ async request(method, path, body, query) {
407
+ const url = buildApiUrl(this.config.apiBaseUrl, path, query);
408
+ let response;
409
+ try {
410
+ response = await fetch(url, {
411
+ method,
412
+ headers: {
413
+ Accept: "application/json",
414
+ Authorization: `Bearer ${this.config.apiKey}`,
415
+ ...body ? { "Content-Type": "application/json" } : {}
416
+ },
417
+ body: body ? JSON.stringify(body) : void 0
418
+ });
419
+ } catch (error) {
420
+ throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
421
+ cause: error instanceof Error ? error : void 0
422
+ });
423
+ }
424
+ const rawText = await response.text();
425
+ if (!rawText) {
426
+ if (!response.ok) {
427
+ throw new CliError(
428
+ `PeakURL request failed with HTTP ${response.status}.`
429
+ );
430
+ }
431
+ return {
432
+ success: true,
433
+ message: "Request completed.",
434
+ data: void 0,
435
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
436
+ };
437
+ }
438
+ let parsed;
439
+ try {
440
+ parsed = JSON.parse(rawText);
441
+ } catch {
442
+ if (!response.ok) {
443
+ throw new CliError(
444
+ `PeakURL request failed with HTTP ${response.status}.`
445
+ );
446
+ }
447
+ throw new CliError("PeakURL returned an invalid JSON response.");
448
+ }
449
+ if (!isApiResponse(parsed)) {
450
+ throw new CliError(
451
+ "PeakURL returned an unexpected response envelope."
452
+ );
453
+ }
454
+ if (!response.ok || !parsed.success) {
455
+ const statusCode = response.status === 401 ? 2 : 1;
456
+ throw new CliError(
457
+ parsed.message || `PeakURL request failed with HTTP ${response.status}.`,
458
+ statusCode
459
+ );
460
+ }
461
+ return parsed;
462
+ }
463
+ };
464
+
465
+ // src/lib/output.ts
466
+ function writeStdout(message = "") {
467
+ process.stdout.write(`${message}
468
+ `);
469
+ }
470
+ function writeStderr(message = "") {
471
+ process.stderr.write(`${message}
472
+ `);
473
+ }
474
+ function outputStream(target) {
475
+ return target === "stdout" ? process.stdout : process.stderr;
476
+ }
477
+ function useColor(target) {
478
+ return Boolean(outputStream(target).isTTY && !process.env.NO_COLOR);
479
+ }
480
+ function successLine(message, target = "stdout") {
481
+ const label = useColor(target) ? "\x1B[32mSuccess\x1B[39m" : "Success";
482
+ return `${label}: ${message}`;
483
+ }
484
+ function errorLine(message, target = "stderr") {
485
+ const label = useColor(target) ? "\x1B[31mError\x1B[39m" : "Error";
486
+ return `${label}: ${message}`;
487
+ }
488
+ function writeNoticeBox(title, lines, target = "stderr") {
489
+ const contentLines = lines.length > 0 ? lines : [""];
490
+ const width = Math.max(
491
+ title.length,
492
+ ...contentLines.map((line) => line.length)
493
+ );
494
+ const stream = outputStream(target);
495
+ const useTuiBox = stream.isTTY;
496
+ const border = useTuiBox ? {
497
+ topLeft: "\u250C",
498
+ topRight: "\u2510",
499
+ bottomLeft: "\u2514",
500
+ bottomRight: "\u2518",
501
+ horizontal: "\u2500",
502
+ vertical: "\u2502",
503
+ separatorLeft: "\u251C",
504
+ separatorRight: "\u2524"
505
+ } : {
506
+ topLeft: "+",
507
+ topRight: "+",
508
+ bottomLeft: "+",
509
+ bottomRight: "+",
510
+ horizontal: "-",
511
+ vertical: "|",
512
+ separatorLeft: "+",
513
+ separatorRight: "+"
514
+ };
515
+ const topBorder = `${border.topLeft}${border.horizontal.repeat(width + 2)}${border.topRight}`;
516
+ const separator = `${border.separatorLeft}${border.horizontal.repeat(width + 2)}${border.separatorRight}`;
517
+ const bottomBorder = `${border.bottomLeft}${border.horizontal.repeat(width + 2)}${border.bottomRight}`;
518
+ const writeLine = target === "stdout" ? writeStdout : writeStderr;
519
+ writeLine(topBorder);
520
+ writeLine(`${border.vertical} ${title.padEnd(width)} ${border.vertical}`);
521
+ writeLine(separator);
522
+ for (const line of contentLines) {
523
+ writeLine(
524
+ `${border.vertical} ${line.padEnd(width)} ${border.vertical}`
525
+ );
526
+ }
527
+ writeLine(bottomBorder);
528
+ }
529
+ function formatTable(headers, rows2, target = "stdout") {
530
+ const stream = outputStream(target);
531
+ const useTuiBox = stream.isTTY;
532
+ const border = useTuiBox ? {
533
+ topLeft: "\u250C",
534
+ topRight: "\u2510",
535
+ bottomLeft: "\u2514",
536
+ bottomRight: "\u2518",
537
+ horizontal: "\u2500",
538
+ vertical: "\u2502",
539
+ separatorLeft: "\u251C",
540
+ separatorRight: "\u2524",
541
+ topJunction: "\u252C",
542
+ middleJunction: "\u253C",
543
+ bottomJunction: "\u2534"
544
+ } : {
545
+ topLeft: "+",
546
+ topRight: "+",
547
+ bottomLeft: "+",
548
+ bottomRight: "+",
549
+ horizontal: "-",
550
+ vertical: "|",
551
+ separatorLeft: "+",
552
+ separatorRight: "+",
553
+ topJunction: "+",
554
+ middleJunction: "+",
555
+ bottomJunction: "+"
556
+ };
557
+ const getLines = (value) => (value ?? "").split("\n");
558
+ const widths = headers.map(
559
+ (header, index) => Math.max(
560
+ header.length,
561
+ ...rows2.flatMap(
562
+ (row2) => getLines(row2[index]).map((line) => line.length)
563
+ )
564
+ )
565
+ );
566
+ const formatTableBorder = (left, join3, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join3)}${right}`;
567
+ const formatTableRow = (cells) => {
568
+ const linesByCell = cells.map(getLines);
569
+ const rowHeight = Math.max(...linesByCell.map((lines) => lines.length));
570
+ return Array.from(
571
+ { length: rowHeight },
572
+ (_value, rowIndex) => `${border.vertical}${linesByCell.map(
573
+ (lines, cellIndex) => ` ${(lines[rowIndex] ?? "").padEnd(widths[cellIndex])} `
574
+ ).join(border.vertical)}${border.vertical}`
575
+ ).join("\n");
576
+ };
577
+ return [
578
+ formatTableBorder(border.topLeft, border.topJunction, border.topRight),
579
+ formatTableRow(headers),
580
+ formatTableBorder(
581
+ border.separatorLeft,
582
+ border.middleJunction,
583
+ border.separatorRight
584
+ ),
585
+ ...rows2.map(formatTableRow),
586
+ formatTableBorder(
587
+ border.bottomLeft,
588
+ border.bottomJunction,
589
+ border.bottomRight
590
+ )
591
+ ].join("\n");
592
+ }
593
+ function formatDetailsTable(rows2, target = "stdout") {
594
+ return formatTable(["Detail", "Information"], rows2, target);
595
+ }
596
+ function writeJson(value) {
597
+ writeStdout(JSON.stringify(value, null, 2));
598
+ }
599
+
600
+ // src/lib/activity.ts
601
+ var ACTIVITY_LIST_KEYS = [
602
+ "items",
603
+ "results",
604
+ "activities",
605
+ "history"
606
+ ];
607
+ function asObject(value) {
608
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
609
+ }
610
+ function asString(value) {
611
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
612
+ }
613
+ function asNumber(value) {
614
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
615
+ }
616
+ function truncate(value, maxLength) {
617
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
618
+ }
619
+ function getActivityMeta(data) {
620
+ const record = asObject(data);
621
+ if (!record) {
622
+ return null;
623
+ }
624
+ const meta = asObject(record.meta);
625
+ if (meta) {
626
+ return {
627
+ page: asNumber(meta.page),
628
+ limit: asNumber(meta.limit),
629
+ totalItems: asNumber(meta.totalItems),
630
+ totalPages: asNumber(meta.totalPages)
631
+ };
632
+ }
633
+ return {
634
+ page: asNumber(record.page),
635
+ limit: asNumber(record.limit),
636
+ totalItems: asNumber(record.total),
637
+ totalPages: asNumber(record.totalPages)
638
+ };
639
+ }
640
+ function extractActivity(data) {
641
+ if (Array.isArray(data)) {
642
+ return data;
643
+ }
644
+ const record = asObject(data);
645
+ if (record) {
646
+ for (const key of ACTIVITY_LIST_KEYS) {
647
+ const value = record[key];
648
+ if (Array.isArray(value)) {
649
+ return value;
650
+ }
651
+ }
652
+ }
653
+ return [];
654
+ }
655
+ function formatActivityTable(items) {
656
+ if (items.length === 0) {
657
+ return "No activity logs found.";
658
+ }
659
+ const headers = [
660
+ "ID",
661
+ "Action",
662
+ "User",
663
+ "IP Address",
664
+ "Timestamp",
665
+ "Message"
666
+ ];
667
+ const rows2 = items.map((item) => [
668
+ truncate(asString(item.id) || "-", 18),
669
+ truncate(asString(item.type) || "-", 18),
670
+ truncate(
671
+ asString(item.userName) || asString(item.userEmail) || "-",
672
+ 18
673
+ ),
674
+ truncate(asString(item.ipAddress) || "-", 16),
675
+ truncate(asString(item.createdAt) || "-", 22),
676
+ truncate(asString(item.message) || "-", 40)
677
+ ]);
678
+ return formatTable(headers, rows2);
679
+ }
680
+ function formatActivitySummary(data, count) {
681
+ const meta = getActivityMeta(data);
682
+ if (!meta) {
683
+ return `${count} activity record${count === 1 ? "" : "s"} returned.`;
32
684
  }
33
- return new CliError("Unexpected error.");
685
+ const total = meta.totalItems;
686
+ const page = meta.page;
687
+ const totalPages = meta.totalPages;
688
+ if (total !== void 0 && page !== void 0 && totalPages !== void 0) {
689
+ return `Page ${page} of ${totalPages}. ${total} total activity record${total === 1 ? "" : "s"}.`;
690
+ }
691
+ return `${count} activity record${count === 1 ? "" : "s"} returned.`;
34
692
  }
35
693
 
36
694
  // src/config/store.ts
695
+ import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
696
+ import { dirname, join } from "path";
697
+ import envPaths from "env-paths";
37
698
  var CONFIG_FILENAME = "config.json";
38
699
  var STATE_FILENAME = "state.json";
39
700
  function getConfigPath() {
@@ -186,80 +847,6 @@ var StateStore = class {
186
847
  }
187
848
  };
188
849
 
189
- // src/lib/url.ts
190
- function validateHttpUrl(parsed, label) {
191
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
192
- throw new CliError(`${label} must use http or https.`);
193
- }
194
- if (parsed.username || parsed.password) {
195
- throw new CliError(`${label} must not include embedded credentials.`);
196
- }
197
- }
198
- function getApiBaseUrl(value) {
199
- const input = value.trim();
200
- if (!input) {
201
- throw new CliError("A PeakURL API base URL is required.");
202
- }
203
- let parsed;
204
- try {
205
- parsed = new URL(input);
206
- } catch {
207
- throw new CliError(`Invalid API base URL: ${value}`);
208
- }
209
- validateHttpUrl(parsed, "PeakURL API base URL");
210
- parsed.hash = "";
211
- parsed.search = "";
212
- const pathname = parsed.pathname.replace(/\/+$/, "");
213
- if (!/\/api\/v1$/i.test(pathname)) {
214
- throw new CliError("PeakURL API base URL must end with /api/v1.");
215
- }
216
- return `${parsed.origin}${pathname}`;
217
- }
218
- function buildApiUrl(apiBaseUrl, path, query) {
219
- const cleanBaseUrl = getApiBaseUrl(apiBaseUrl);
220
- const cleanPath = path.replace(/^\/+/, "");
221
- const url = new URL(cleanPath, `${cleanBaseUrl}/`);
222
- for (const [key, value] of Object.entries(query ?? {})) {
223
- if (value === void 0 || value === "") {
224
- continue;
225
- }
226
- url.searchParams.set(key, String(value));
227
- }
228
- return url.toString();
229
- }
230
- function normalizeDestinationUrl(value) {
231
- const input = value.trim();
232
- if (!input) {
233
- throw new CliError("A destination URL is required.");
234
- }
235
- try {
236
- const parsed = new URL(input);
237
- validateHttpUrl(parsed, "Destination URL");
238
- return parsed.toString();
239
- } catch (error) {
240
- if (error instanceof CliError) {
241
- throw error;
242
- }
243
- throw new CliError(`Invalid destination URL: ${value}`);
244
- }
245
- }
246
- function normalizeWebhookUrl(value) {
247
- const input = value.trim();
248
- if (!input) {
249
- throw new CliError("A webhook URL is required.");
250
- }
251
- try {
252
- const parsed = new URL(input);
253
- validateHttpUrl(parsed, "Webhook URL");
254
- return parsed.toString();
255
- } catch (error) {
256
- if (error instanceof CliError) {
257
- throw error;
258
- }
259
- throw new CliError(`Invalid webhook URL: ${value}`);
260
- }
261
- }
262
-
263
850
  // src/lib/auth.ts
264
851
  var AUTH_REQUIRED_MESSAGE = "PeakURL credentials are not configured.";
265
852
  var EXAMPLE_BASE_URL = "https://example.com/api/v1";
@@ -326,143 +913,6 @@ import { tmpdir } from "os";
326
913
  import { dirname as dirname2, join as join2, resolve } from "path";
327
914
  import { posix as pathPosix } from "path";
328
915
  import { inflateRawSync } from "zlib";
329
-
330
- // src/lib/output.ts
331
- function writeStdout(message = "") {
332
- process.stdout.write(`${message}
333
- `);
334
- }
335
- function writeStderr(message = "") {
336
- process.stderr.write(`${message}
337
- `);
338
- }
339
- function outputStream(target) {
340
- return target === "stdout" ? process.stdout : process.stderr;
341
- }
342
- function useColor(target) {
343
- return Boolean(outputStream(target).isTTY && !process.env.NO_COLOR);
344
- }
345
- function successLine(message, target = "stdout") {
346
- const label = useColor(target) ? "\x1B[32mSuccess\x1B[39m" : "Success";
347
- return `${label}: ${message}`;
348
- }
349
- function errorLine(message, target = "stderr") {
350
- const label = useColor(target) ? "\x1B[31mError\x1B[39m" : "Error";
351
- return `${label}: ${message}`;
352
- }
353
- function writeNoticeBox(title, lines, target = "stderr") {
354
- const contentLines = lines.length > 0 ? lines : [""];
355
- const width = Math.max(
356
- title.length,
357
- ...contentLines.map((line) => line.length)
358
- );
359
- const stream = outputStream(target);
360
- const useTuiBox = stream.isTTY;
361
- const border = useTuiBox ? {
362
- topLeft: "\u250C",
363
- topRight: "\u2510",
364
- bottomLeft: "\u2514",
365
- bottomRight: "\u2518",
366
- horizontal: "\u2500",
367
- vertical: "\u2502",
368
- separatorLeft: "\u251C",
369
- separatorRight: "\u2524"
370
- } : {
371
- topLeft: "+",
372
- topRight: "+",
373
- bottomLeft: "+",
374
- bottomRight: "+",
375
- horizontal: "-",
376
- vertical: "|",
377
- separatorLeft: "+",
378
- separatorRight: "+"
379
- };
380
- const topBorder = `${border.topLeft}${border.horizontal.repeat(width + 2)}${border.topRight}`;
381
- const separator = `${border.separatorLeft}${border.horizontal.repeat(width + 2)}${border.separatorRight}`;
382
- const bottomBorder = `${border.bottomLeft}${border.horizontal.repeat(width + 2)}${border.bottomRight}`;
383
- const writeLine = target === "stdout" ? writeStdout : writeStderr;
384
- writeLine(topBorder);
385
- writeLine(`${border.vertical} ${title.padEnd(width)} ${border.vertical}`);
386
- writeLine(separator);
387
- for (const line of contentLines) {
388
- writeLine(
389
- `${border.vertical} ${line.padEnd(width)} ${border.vertical}`
390
- );
391
- }
392
- writeLine(bottomBorder);
393
- }
394
- function formatTable(headers, rows2, target = "stdout") {
395
- const stream = outputStream(target);
396
- const useTuiBox = stream.isTTY;
397
- const border = useTuiBox ? {
398
- topLeft: "\u250C",
399
- topRight: "\u2510",
400
- bottomLeft: "\u2514",
401
- bottomRight: "\u2518",
402
- horizontal: "\u2500",
403
- vertical: "\u2502",
404
- separatorLeft: "\u251C",
405
- separatorRight: "\u2524",
406
- topJunction: "\u252C",
407
- middleJunction: "\u253C",
408
- bottomJunction: "\u2534"
409
- } : {
410
- topLeft: "+",
411
- topRight: "+",
412
- bottomLeft: "+",
413
- bottomRight: "+",
414
- horizontal: "-",
415
- vertical: "|",
416
- separatorLeft: "+",
417
- separatorRight: "+",
418
- topJunction: "+",
419
- middleJunction: "+",
420
- bottomJunction: "+"
421
- };
422
- const getLines = (value) => (value ?? "").split("\n");
423
- const widths = headers.map(
424
- (header, index) => Math.max(
425
- header.length,
426
- ...rows2.flatMap(
427
- (row2) => getLines(row2[index]).map((line) => line.length)
428
- )
429
- )
430
- );
431
- const formatTableBorder = (left, join3, right) => `${left}${widths.map((width) => border.horizontal.repeat(width + 2)).join(join3)}${right}`;
432
- const formatTableRow = (cells) => {
433
- const linesByCell = cells.map(getLines);
434
- const rowHeight = Math.max(...linesByCell.map((lines) => lines.length));
435
- return Array.from(
436
- { length: rowHeight },
437
- (_value, rowIndex) => `${border.vertical}${linesByCell.map(
438
- (lines, cellIndex) => ` ${(lines[rowIndex] ?? "").padEnd(widths[cellIndex])} `
439
- ).join(border.vertical)}${border.vertical}`
440
- ).join("\n");
441
- };
442
- return [
443
- formatTableBorder(border.topLeft, border.topJunction, border.topRight),
444
- formatTableRow(headers),
445
- formatTableBorder(
446
- border.separatorLeft,
447
- border.middleJunction,
448
- border.separatorRight
449
- ),
450
- ...rows2.map(formatTableRow),
451
- formatTableBorder(
452
- border.bottomLeft,
453
- border.bottomJunction,
454
- border.bottomRight
455
- )
456
- ].join("\n");
457
- }
458
- function formatDetailsTable(rows2, target = "stdout") {
459
- return formatTable(["Detail", "Information"], rows2, target);
460
- }
461
- function writeJson(value) {
462
- writeStdout(JSON.stringify(value, null, 2));
463
- }
464
-
465
- // src/lib/core.ts
466
916
  var DEFAULT_RELEASE_API_URL = "https://api.peakurl.org/v1/update";
467
917
  var DEFAULT_CORE_PACKAGE_URL = "https://peakurl.org/latest.zip";
468
918
  var EOCD_SIGNATURE = 101010256;
@@ -481,7 +931,7 @@ function getCorePackageUrl(env) {
481
931
  "package download"
482
932
  );
483
933
  }
484
- function asString(value) {
934
+ function asString2(value) {
485
935
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
486
936
  }
487
937
  function validateUrl(value, label) {
@@ -500,7 +950,7 @@ function validateUrl(value, label) {
500
950
  return parsed.toString();
501
951
  }
502
952
  function normalizeSha256(value) {
503
- const candidate = asString(value)?.toLowerCase();
953
+ const candidate = asString2(value)?.toLowerCase();
504
954
  if (!candidate || !/^[a-f0-9]{64}$/.test(candidate)) {
505
955
  throw new CliError(
506
956
  "PeakURL release metadata is missing a valid SHA-256 checksum."
@@ -718,13 +1168,13 @@ async function getCoreRelease(env) {
718
1168
  throw new CliError("PeakURL release metadata could not be loaded.");
719
1169
  }
720
1170
  const payload = await response.json();
721
- const version = asString(payload.version) || "latest";
1171
+ const version = asString2(payload.version) || "latest";
722
1172
  return {
723
1173
  version,
724
1174
  downloadUrl: getCorePackageUrl(env),
725
1175
  checksumSha256: normalizeSha256(payload.checksumSha256),
726
- releasedAt: asString(payload.releasedAt),
727
- releaseNotesUrl: asString(payload.releaseNotesUrl)
1176
+ releasedAt: asString2(payload.releasedAt),
1177
+ releaseNotesUrl: asString2(payload.releaseNotesUrl)
728
1178
  };
729
1179
  }
730
1180
  async function downloadCorePackage(release, targetPath, force = false) {
@@ -793,17 +1243,23 @@ var EXPORT_HEADERS = [
793
1243
  "created_at"
794
1244
  ];
795
1245
  function text(value) {
796
- return typeof value === "string" ? value : "";
1246
+ if (typeof value === "string") {
1247
+ return value;
1248
+ }
1249
+ if (typeof value === "number" || typeof value === "boolean") {
1250
+ return String(value);
1251
+ }
1252
+ return "";
797
1253
  }
798
1254
  function csvValue(value) {
799
- const content = value == null ? "" : String(value);
1255
+ const content = value === null || value === void 0 ? "" : text(value);
800
1256
  if (/[",\r\n]/.test(content)) {
801
1257
  return `"${content.replace(/"/g, '""')}"`;
802
1258
  }
803
1259
  return content;
804
1260
  }
805
1261
  function xmlValue(value) {
806
- return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1262
+ return text(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
807
1263
  }
808
1264
  function aliasValue(link) {
809
1265
  return text(link.alias) || text(link.shortCode);
@@ -1093,7 +1549,7 @@ function getImportFormat(filePath, value) {
1093
1549
  );
1094
1550
  }
1095
1551
  async function readImportRows(filePath, format) {
1096
- let textContent = "";
1552
+ let textContent;
1097
1553
  try {
1098
1554
  textContent = await readFile2(filePath, "utf8");
1099
1555
  } catch (error) {
@@ -1138,53 +1594,53 @@ function formatImportSummary(data) {
1138
1594
 
1139
1595
  // src/lib/links.ts
1140
1596
  var LIST_KEYS = ["urls", "items", "results"];
1141
- function asObject(value) {
1597
+ function asObject2(value) {
1142
1598
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1143
1599
  }
1144
- function asString2(value) {
1600
+ function asString3(value) {
1145
1601
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1146
1602
  }
1147
- function asNumber(value) {
1603
+ function asNumber2(value) {
1148
1604
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1149
1605
  }
1150
1606
  function pickText(link, keys) {
1151
1607
  for (const key of keys) {
1152
- const value = asString2(link[key]);
1608
+ const value = asString3(link[key]);
1153
1609
  if (value) {
1154
1610
  return value;
1155
1611
  }
1156
1612
  }
1157
1613
  return void 0;
1158
1614
  }
1159
- function truncate(value, maxLength) {
1615
+ function truncate2(value, maxLength) {
1160
1616
  return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
1161
1617
  }
1162
1618
  function getListMeta(data) {
1163
- const record = asObject(data);
1619
+ const record = asObject2(data);
1164
1620
  if (!record) {
1165
1621
  return null;
1166
1622
  }
1167
- const meta = asObject(record.meta);
1623
+ const meta = asObject2(record.meta);
1168
1624
  if (meta) {
1169
1625
  return {
1170
- page: asNumber(meta.page),
1171
- limit: asNumber(meta.limit),
1172
- totalItems: asNumber(meta.totalItems),
1173
- totalPages: asNumber(meta.totalPages)
1626
+ page: asNumber2(meta.page),
1627
+ limit: asNumber2(meta.limit),
1628
+ totalItems: asNumber2(meta.totalItems),
1629
+ totalPages: asNumber2(meta.totalPages)
1174
1630
  };
1175
1631
  }
1176
1632
  return {
1177
- page: asNumber(record.page),
1178
- limit: asNumber(record.limit),
1179
- totalItems: asNumber(record.total),
1180
- totalPages: asNumber(record.totalPages)
1633
+ page: asNumber2(record.page),
1634
+ limit: asNumber2(record.limit),
1635
+ totalItems: asNumber2(record.total),
1636
+ totalPages: asNumber2(record.totalPages)
1181
1637
  };
1182
1638
  }
1183
1639
  function extractLinks(data) {
1184
1640
  if (Array.isArray(data)) {
1185
1641
  return data;
1186
1642
  }
1187
- const record = asObject(data);
1643
+ const record = asObject2(data);
1188
1644
  if (record) {
1189
1645
  for (const key of LIST_KEYS) {
1190
1646
  const value = record[key];
@@ -1221,14 +1677,14 @@ function formatLinkDetails(link) {
1221
1677
  ["Alias", getLinkAlias(link)],
1222
1678
  ["Short URL", getLinkShortUrl(link)],
1223
1679
  ["Destination", getLinkDestination(link)],
1224
- ["Title", asString2(link.title)],
1225
- ["Status", asString2(link.status)],
1680
+ ["Title", asString3(link.title)],
1681
+ ["Status", asString3(link.status)],
1226
1682
  [
1227
1683
  "Clicks",
1228
- asNumber(link.clicks) === void 0 ? void 0 : String(link.clicks)
1684
+ asNumber2(link.clicks) === void 0 ? void 0 : String(link.clicks)
1229
1685
  ],
1230
- ["Created", asString2(link.createdAt)],
1231
- ["Updated", asString2(link.updatedAt)]
1686
+ ["Created", asString3(link.createdAt)],
1687
+ ["Updated", asString3(link.updatedAt)]
1232
1688
  ].filter((entry) => Boolean(entry[1]));
1233
1689
  if (rows2.length === 0) {
1234
1690
  return "No link fields returned.";
@@ -1241,11 +1697,11 @@ function formatLinksTable(links) {
1241
1697
  }
1242
1698
  const headers = ["ID", "Alias", "Short URL", "Destination", "Status"];
1243
1699
  const rows2 = links.map((link) => [
1244
- truncate(getLinkId(link) || "-", 18),
1245
- truncate(getLinkAlias(link) || "-", 12),
1246
- truncate(getLinkShortUrl(link) || "-", 36),
1247
- truncate(getLinkDestination(link) || "-", 52),
1248
- truncate(asString2(link.status) || "-", 12)
1700
+ truncate2(getLinkId(link) || "-", 18),
1701
+ truncate2(getLinkAlias(link) || "-", 12),
1702
+ truncate2(getLinkShortUrl(link) || "-", 36),
1703
+ truncate2(getLinkDestination(link) || "-", 52),
1704
+ truncate2(asString3(link.status) || "-", 12)
1249
1705
  ]);
1250
1706
  return formatTable(headers, rows2);
1251
1707
  }
@@ -1527,6 +1983,33 @@ function locationRows(location) {
1527
1983
  row("Download command", text3(location.downloadCommand), 60)
1528
1984
  ]);
1529
1985
  }
1986
+ function cacheRows(cache) {
1987
+ if (!cache) {
1988
+ return [];
1989
+ }
1990
+ const redisEndpoint = cache.redis?.available || cache.redis?.configured ? `${text3(cache.redis.host) ?? "127.0.0.1"}:${formatCount(cache.redis.port) ?? "6379"}` : void 0;
1991
+ const redisStatus = cache.redis?.available ? cache.redis.serverVersion ? `Connected (v${cache.redis.serverVersion})` : "Connected" : cache.redis?.configured ? "Configured, unavailable" : void 0;
1992
+ const apcuStatus = cache.apcu?.available ? "Available" : cache.apcu?.extensionLoaded ? "Loaded, disabled" : cache.apcu !== void 0 && cache.apcu !== null ? "Missing" : void 0;
1993
+ return rows([
1994
+ row("Status", formatState(cache.status)),
1995
+ row("Enabled", yesNo(cache.enabled, "Enabled", "Disabled")),
1996
+ row("Active driver", text3(cache.activeDriver)),
1997
+ row("Configured driver", text3(cache.configuredDriver)),
1998
+ row("Cache size", formatSize(cache.sizeBytes)),
1999
+ row(
2000
+ cache.activeDriver === "redis" || cache.activeDriver === "apcu" ? "Cached items" : "Cached files",
2001
+ formatCount(cache.fileCount)
2002
+ ),
2003
+ row("Default TTL", formatSeconds(cache.defaultTtl)),
2004
+ row("Negative TTL", formatSeconds(cache.negativeTtl)),
2005
+ row("Cache directory", text3(cache.path), 60),
2006
+ row("Directory exists", yesNo(cache.directoryExists)),
2007
+ row("Directory writable", yesNo(cache.writable)),
2008
+ row("Redis server", redisEndpoint),
2009
+ row("Redis status", redisStatus),
2010
+ row("APCu extension", apcuStatus)
2011
+ ]);
2012
+ }
1530
2013
  function dataRows(data) {
1531
2014
  if (!data) {
1532
2015
  return [];
@@ -1575,11 +2058,127 @@ ${checks}` : void 0,
1575
2058
  section("Storage", storageRows(status2.storage)),
1576
2059
  section("Mail", mailRows(status2.mail)),
1577
2060
  section("Location", locationRows(status2.location)),
2061
+ section("Cache", cacheRows(status2.cache)),
1578
2062
  section("Data", dataRows(status2.data))
1579
2063
  ].filter((value) => Boolean(value));
1580
2064
  return sections.length > 0 ? sections.join("\n\n") : "No system status fields returned.";
1581
2065
  }
1582
2066
 
2067
+ // src/lib/job.ts
2068
+ function formatInterval(seconds) {
2069
+ if (seconds < 60) return `${seconds}s`;
2070
+ const minutes = Math.floor(seconds / 60);
2071
+ if (minutes < 60) return `${minutes}m`;
2072
+ const hours = Math.floor(minutes / 60);
2073
+ if (hours < 24) return `${hours}h`;
2074
+ const days = Math.floor(hours / 24);
2075
+ return `${days}d`;
2076
+ }
2077
+ function formatDate(date) {
2078
+ if (!date) return "never";
2079
+ return new Date(date).toISOString().replace("T", " ").substring(0, 19);
2080
+ }
2081
+ function formatJobsList(status2) {
2082
+ const rows2 = status2.jobs.map((job) => [
2083
+ job.id,
2084
+ job.title,
2085
+ job.status,
2086
+ job.is_enabled ? "yes" : "no",
2087
+ formatDate(job.next_run_at)
2088
+ ]);
2089
+ const table = formatTable(
2090
+ ["ID", "Job", "Status", "Enabled", "Next Run"],
2091
+ rows2
2092
+ );
2093
+ const summary = `
2094
+ ${status2.jobs_count} jobs registered.
2095
+ Timezone: ${status2.timezone}
2096
+ History retention: ${status2.retention_days} days`;
2097
+ return `${table}${summary}`;
2098
+ }
2099
+ function formatJobDetails(job) {
2100
+ const rows2 = [
2101
+ ["ID", job.id],
2102
+ ["Title", job.title],
2103
+ ["Status", job.status],
2104
+ ["Enabled", job.is_enabled ? "yes" : "no"],
2105
+ ["Current interval", formatInterval(job.interval_seconds)],
2106
+ [
2107
+ "Recommended interval",
2108
+ formatInterval(job.recommended_interval_seconds)
2109
+ ],
2110
+ ["Preferred run time", job.preferred_run_time || "none"],
2111
+ ["Customized", job.is_customized ? "yes" : "no"],
2112
+ ["Next run", formatDate(job.next_run_at)],
2113
+ ["Last run", formatDate(job.last_run_at)],
2114
+ ["Last finished", formatDate(job.last_finished_at)],
2115
+ ["Attempts", String(job.attempts)],
2116
+ ["Maximum attempts", String(job.max_attempts)],
2117
+ ["Last error", job.last_error || "none"]
2118
+ ];
2119
+ let out = formatDetailsTable(rows2);
2120
+ if (job.recent_runs && job.recent_runs.length > 0) {
2121
+ out += `
2122
+
2123
+ Recent Runs:
2124
+ ${formatJobHistory(job.recent_runs)}`;
2125
+ }
2126
+ return out;
2127
+ }
2128
+ function formatJobHistory(runs) {
2129
+ if (!runs || runs.length === 0) {
2130
+ return "No recent runs.";
2131
+ }
2132
+ const rows2 = runs.map((run) => [
2133
+ run.id,
2134
+ run.status,
2135
+ String(run.attempt),
2136
+ formatDate(run.started_at),
2137
+ formatDate(run.finished_at),
2138
+ run.duration_ms ? `${run.duration_ms}ms` : "-",
2139
+ run.output_summary || "-",
2140
+ run.error_message || "-"
2141
+ ]);
2142
+ return formatTable(
2143
+ [
2144
+ "Run ID",
2145
+ "Status",
2146
+ "Attempt",
2147
+ "Started",
2148
+ "Finished",
2149
+ "Duration",
2150
+ "Summary",
2151
+ "Error"
2152
+ ],
2153
+ rows2
2154
+ );
2155
+ }
2156
+ function formatRunJobResult(result) {
2157
+ let out = `Job: ${result.job_id}
2158
+ Status: ${result.status}`;
2159
+ if (result.summary) {
2160
+ out += `
2161
+ Summary: ${result.summary}`;
2162
+ }
2163
+ if (result.error) {
2164
+ out += `
2165
+ Error: ${result.error}`;
2166
+ }
2167
+ return out;
2168
+ }
2169
+ function formatRunDueResult(result) {
2170
+ if (!result.results || result.results.length === 0) {
2171
+ return "No jobs were due.";
2172
+ }
2173
+ const rows2 = result.results.map((r) => [
2174
+ r.job_id,
2175
+ r.status,
2176
+ r.summary || "-",
2177
+ r.error || "-"
2178
+ ]);
2179
+ return formatTable(["Job ID", "Status", "Summary", "Error"], rows2);
2180
+ }
2181
+
1583
2182
  // src/lib/update.ts
1584
2183
  var PACKAGE_NAME = "peakurl";
1585
2184
  var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
@@ -1846,7 +2445,7 @@ function text5(value) {
1846
2445
  function textList(value) {
1847
2446
  return Array.isArray(value) ? value.map((item) => text5(item)).filter((item) => Boolean(item)) : [];
1848
2447
  }
1849
- function truncate2(value, maxLength) {
2448
+ function truncate3(value, maxLength) {
1850
2449
  return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
1851
2450
  }
1852
2451
  function parseWebhookEvents(value, previous = []) {
@@ -1893,11 +2492,11 @@ function formatWebhooksTable(webhooks) {
1893
2492
  return formatTable(
1894
2493
  ["ID", "URL", "Events", "Status", "Secret"],
1895
2494
  webhooks.map((webhook) => [
1896
- truncate2(getWebhookId(webhook) || "-", 18),
1897
- truncate2(getWebhookUrl(webhook) || "-", 42),
1898
- truncate2(getWebhookEvents(webhook).join(", ") || "-", 30),
2495
+ truncate3(getWebhookId(webhook) || "-", 18),
2496
+ truncate3(getWebhookUrl(webhook) || "-", 42),
2497
+ truncate3(getWebhookEvents(webhook).join(", ") || "-", 30),
1899
2498
  webhook.isActive === false ? "inactive" : "active",
1900
- truncate2(text5(webhook.secretHint) || "-", 18)
2499
+ truncate3(text5(webhook.secretHint) || "-", 18)
1901
2500
  ])
1902
2501
  );
1903
2502
  }
@@ -1923,249 +2522,141 @@ function formatWebhooksSummary(webhooks) {
1923
2522
  return `${webhooks.length} webhook${webhooks.length === 1 ? "" : "s"} returned.`;
1924
2523
  }
1925
2524
 
1926
- // src/commands/core.ts
1927
- async function downloadCore(options) {
1928
- const release = await getCoreRelease(process.env);
1929
- const result = await downloadCorePackage(
1930
- release,
1931
- cwd(),
1932
- Boolean(options.force)
1933
- );
1934
- const responseBody = {
1935
- success: true,
1936
- message: "PeakURL downloaded.",
1937
- data: result,
1938
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1939
- };
2525
+ // src/commands/activity.ts
2526
+ async function listActivity(options) {
2527
+ const config = await getAuthConfig(process.env);
2528
+ const response = await new ApiClient(config).listActivity({
2529
+ page: options.page,
2530
+ limit: options.limit,
2531
+ search: options.search
2532
+ });
2533
+ const items = extractActivity(response.data);
1940
2534
  if (options.json) {
1941
- writeJson(responseBody);
2535
+ writeJson(response);
1942
2536
  return;
1943
2537
  }
1944
2538
  if (options.quiet) {
1945
- writeStdout(result.path);
2539
+ for (const item of items) {
2540
+ if (item.id) {
2541
+ writeStdout(String(item.id));
2542
+ }
2543
+ }
1946
2544
  return;
1947
2545
  }
1948
- writeStdout(successLine(responseBody.message));
1949
- writeStdout(formatCoreDownload(result));
1950
- }
1951
-
1952
- // src/commands/links.ts
1953
- import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1954
- import { dirname as dirname3, resolve as resolve2 } from "path";
1955
-
1956
- // src/api/client.ts
1957
- function isApiResponse(value) {
1958
- return Boolean(
1959
- value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
1960
- );
1961
- }
1962
- function networkError(apiBaseUrl, error) {
1963
- if (error instanceof Error && error.message) {
1964
- return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
1965
- }
1966
- return `Could not reach PeakURL at ${apiBaseUrl}.`;
2546
+ writeStdout(successLine(response.message || "Activity loaded."));
2547
+ writeStdout(formatActivityTable(items));
2548
+ writeStdout(formatActivitySummary(response.data, items.length));
1967
2549
  }
1968
- var ApiClient = class {
1969
- /**
1970
- * Creates a client bound to one resolved credential set.
1971
- *
1972
- * @param config Explicit API base URL plus bearer API key.
1973
- */
1974
- constructor(config) {
1975
- this.config = config;
1976
- }
1977
- config;
1978
- /**
1979
- * Loads the currently authenticated user.
1980
- *
1981
- * PeakURL accepts bearer API keys on `GET /users/me`, which is also the
1982
- * CLI login verification flow.
1983
- *
1984
- * @returns API response envelope containing the authenticated user.
1985
- */
1986
- whoami() {
1987
- return this.request("GET", "users/me");
1988
- }
1989
- /**
1990
- * Loads the current system status snapshot for the authenticated site.
1991
- *
1992
- * @returns API response envelope containing system status sections.
1993
- */
1994
- getStatus() {
1995
- return this.request("GET", "system/status");
1996
- }
1997
- /**
1998
- * Creates a short URL.
1999
- *
2000
- * @param payload Request body accepted by `POST /api/v1/urls`.
2001
- * @returns API response envelope containing the created link.
2002
- */
2003
- createUrl(payload) {
2004
- return this.request("POST", "urls", payload);
2005
- }
2006
- /**
2007
- * Lists short URLs with optional pagination and filtering.
2008
- *
2009
- * The current PeakURL app returns `{ items, meta }` under `data`, but the
2010
- * CLI keeps a slightly broader compatibility type for future-proofing.
2011
- *
2012
- * @param query Optional query-string values.
2013
- * @returns API response envelope containing list data.
2014
- */
2015
- listUrls(query) {
2016
- return this.request("GET", "urls", void 0, query);
2017
- }
2018
- /**
2019
- * Exports the full accessible link dataset for the authenticated user.
2020
- *
2021
- * @param query Optional search and sort values.
2022
- * @returns API response envelope containing the full export payload.
2023
- */
2024
- exportUrls(query) {
2025
- return this.request(
2026
- "GET",
2027
- "urls/export",
2028
- void 0,
2029
- query
2030
- );
2031
- }
2032
- /**
2033
- * Imports multiple short links in one bulk request.
2034
- *
2035
- * @param payload Request body accepted by `POST /api/v1/urls/bulk`.
2036
- * @returns API response envelope containing created rows plus row errors.
2037
- */
2038
- importUrls(payload) {
2039
- return this.request("POST", "urls/bulk", payload);
2040
- }
2041
- /**
2042
- * Loads a single short URL by identifier or alias.
2043
- *
2044
- * PeakURL resolves IDs, short codes, and aliases through the same route.
2045
- *
2046
- * @param idOrAlias Link identifier, short code, or alias.
2047
- * @returns API response envelope containing the resolved link.
2048
- */
2049
- getUrl(idOrAlias) {
2050
- return this.request(
2051
- "GET",
2052
- `urls/${encodeURIComponent(idOrAlias)}`
2053
- );
2054
- }
2055
- /**
2056
- * Deletes a short URL by its stable row ID.
2057
- *
2058
- * The current PeakURL backend delete route expects the row ID. The CLI can
2059
- * still accept an alias at the command layer by resolving it first.
2060
- *
2061
- * @param id Stable link row ID.
2062
- * @returns API response envelope containing the deletion result.
2063
- */
2064
- deleteUrl(id) {
2065
- return this.request(
2066
- "DELETE",
2067
- `urls/${encodeURIComponent(id)}`
2068
- );
2069
- }
2070
- /**
2071
- * Lists outbound webhooks for the authenticated user.
2072
- *
2073
- * @returns API response envelope containing webhook rows.
2074
- */
2075
- listWebhooks() {
2076
- return this.request("GET", "webhooks");
2077
- }
2078
- /**
2079
- * Creates one outbound webhook subscription.
2080
- *
2081
- * @param payload Request body accepted by `POST /api/v1/webhooks`.
2082
- * @returns API response envelope containing the created webhook.
2083
- */
2084
- createWebhook(payload) {
2085
- return this.request("POST", "webhooks", payload);
2086
- }
2087
- /**
2088
- * Deletes one webhook by its stable row ID.
2089
- *
2090
- * @param id Webhook identifier returned by the list/create endpoints.
2091
- * @returns API response envelope containing the deletion result.
2092
- */
2093
- deleteWebhook(id) {
2094
- return this.request(
2095
- "DELETE",
2096
- `webhooks/${encodeURIComponent(id)}`
2550
+ async function deleteActivity(identifiers, options) {
2551
+ const config = await getAuthConfig(process.env);
2552
+ const client = new ApiClient(config);
2553
+ if (options.all) {
2554
+ const response2 = await client.clearActivity();
2555
+ if (options.json) {
2556
+ writeJson(response2);
2557
+ return;
2558
+ }
2559
+ if (options.quiet) {
2560
+ return;
2561
+ }
2562
+ writeStdout(
2563
+ successLine(response2.message || "All activity logs deleted.")
2097
2564
  );
2565
+ return;
2098
2566
  }
2099
- /**
2100
- * Performs one authenticated API request and normalizes the response.
2101
- *
2102
- * @param method HTTP method to send.
2103
- * @param path Route path relative to `/api/v1`.
2104
- * @param body Optional JSON body.
2105
- * @param query Optional query-string values.
2106
- * @returns Parsed PeakURL response envelope.
2107
- * @throws {CliError} When the network request fails or the API returns an error.
2108
- */
2109
- async request(method, path, body, query) {
2110
- const url = buildApiUrl(this.config.apiBaseUrl, path, query);
2111
- let response;
2112
- try {
2113
- response = await fetch(url, {
2114
- method,
2115
- headers: {
2116
- Accept: "application/json",
2117
- Authorization: `Bearer ${this.config.apiKey}`,
2118
- ...body ? { "Content-Type": "application/json" } : {}
2119
- },
2120
- body: body ? JSON.stringify(body) : void 0
2121
- });
2122
- } catch (error) {
2123
- throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
2124
- cause: error instanceof Error ? error : void 0
2125
- });
2126
- }
2127
- const rawText = await response.text();
2128
- if (!rawText) {
2129
- if (!response.ok) {
2130
- throw new CliError(
2131
- `PeakURL request failed with HTTP ${response.status}.`
2132
- );
2567
+ const rawTargets = [];
2568
+ if (typeof identifiers === "string" && identifiers.trim()) {
2569
+ rawTargets.push(identifiers.trim());
2570
+ } else if (Array.isArray(identifiers)) {
2571
+ for (const item of identifiers) {
2572
+ if (typeof item === "string" && item.trim()) {
2573
+ rawTargets.push(item.trim());
2133
2574
  }
2134
- return {
2135
- success: true,
2136
- message: "Request completed.",
2137
- data: void 0,
2138
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2139
- };
2140
2575
  }
2141
- let parsed;
2142
- try {
2143
- parsed = JSON.parse(rawText);
2144
- } catch {
2145
- if (!response.ok) {
2146
- throw new CliError(
2147
- `PeakURL request failed with HTTP ${response.status}.`
2148
- );
2576
+ }
2577
+ if (options.ids) {
2578
+ for (const id of options.ids.split(",")) {
2579
+ const trimmed = id.trim();
2580
+ if (trimmed) {
2581
+ rawTargets.push(trimmed);
2149
2582
  }
2150
- throw new CliError("PeakURL returned an invalid JSON response.");
2151
2583
  }
2152
- if (!isApiResponse(parsed)) {
2153
- throw new CliError(
2154
- "PeakURL returned an unexpected response envelope."
2155
- );
2584
+ }
2585
+ const uniqueTargets = Array.from(new Set(rawTargets));
2586
+ if (uniqueTargets.length === 0) {
2587
+ throw new CliError(
2588
+ "Specify one or more activity IDs to delete, or use --all to delete all activity logs."
2589
+ );
2590
+ }
2591
+ if (uniqueTargets.length === 1) {
2592
+ const response2 = await client.deleteActivity(uniqueTargets[0]);
2593
+ if (options.json) {
2594
+ writeJson(response2);
2595
+ return;
2156
2596
  }
2157
- if (!response.ok || !parsed.success) {
2158
- const statusCode = response.status === 401 ? 2 : 1;
2159
- throw new CliError(
2160
- parsed.message || `PeakURL request failed with HTTP ${response.status}.`,
2161
- statusCode
2162
- );
2597
+ if (options.quiet) {
2598
+ return;
2163
2599
  }
2164
- return parsed;
2600
+ writeStdout(successLine(response2.message || "Activity log deleted."));
2601
+ return;
2165
2602
  }
2166
- };
2603
+ const response = await client.deleteActivityBulk(uniqueTargets);
2604
+ if (options.json) {
2605
+ writeJson(response);
2606
+ return;
2607
+ }
2608
+ if (options.quiet) {
2609
+ return;
2610
+ }
2611
+ writeStdout(
2612
+ successLine(
2613
+ response.message || `Deleted ${uniqueTargets.length} activity record${uniqueTargets.length === 1 ? "" : "s"}.`
2614
+ )
2615
+ );
2616
+ }
2617
+ async function clearActivity(options) {
2618
+ const config = await getAuthConfig(process.env);
2619
+ const response = await new ApiClient(config).clearActivity();
2620
+ if (options.json) {
2621
+ writeJson(response);
2622
+ return;
2623
+ }
2624
+ if (options.quiet) {
2625
+ return;
2626
+ }
2627
+ writeStdout(successLine(response.message || "All activity logs deleted."));
2628
+ }
2629
+
2630
+ // src/commands/core.ts
2631
+ import { cwd } from "process";
2632
+ async function downloadCore(options) {
2633
+ const release = await getCoreRelease(process.env);
2634
+ const result = await downloadCorePackage(
2635
+ release,
2636
+ cwd(),
2637
+ Boolean(options.force)
2638
+ );
2639
+ const responseBody = {
2640
+ success: true,
2641
+ message: "PeakURL downloaded.",
2642
+ data: result,
2643
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2644
+ };
2645
+ if (options.json) {
2646
+ writeJson(responseBody);
2647
+ return;
2648
+ }
2649
+ if (options.quiet) {
2650
+ writeStdout(result.path);
2651
+ return;
2652
+ }
2653
+ writeStdout(successLine(responseBody.message));
2654
+ writeStdout(formatCoreDownload(result));
2655
+ }
2167
2656
 
2168
2657
  // src/commands/links.ts
2658
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
2659
+ import { dirname as dirname3, resolve as resolve2 } from "path";
2169
2660
  function normalizeExpiresAt(value) {
2170
2661
  if (!value) {
2171
2662
  return void 0;
@@ -2308,17 +2799,93 @@ async function getLink(idOrAlias, options) {
2308
2799
  writeStdout(successLine(response.message));
2309
2800
  writeStdout(formatLinkDetails(response.data));
2310
2801
  }
2311
- async function deleteLink(idOrAlias, options) {
2802
+ async function deleteLink(identifiers, options) {
2312
2803
  const config = await getAuthConfig(process.env);
2313
2804
  const client = new ApiClient(config);
2314
- const lookupResponse = await client.getUrl(idOrAlias);
2315
- const resolvedId = getLinkId(lookupResponse.data);
2316
- if (!resolvedId) {
2805
+ if (options.all) {
2806
+ const response2 = await client.clearUrls();
2807
+ if (options.json) {
2808
+ writeJson(response2);
2809
+ return;
2810
+ }
2811
+ if (options.quiet) {
2812
+ return;
2813
+ }
2814
+ writeStdout(
2815
+ successLine(response2.message || "All short links deleted.")
2816
+ );
2817
+ return;
2818
+ }
2819
+ if (options.trash || options.emptyTrash) {
2820
+ const response2 = await client.emptyTrash();
2821
+ if (options.json) {
2822
+ writeJson(response2);
2823
+ return;
2824
+ }
2825
+ if (options.quiet) {
2826
+ return;
2827
+ }
2828
+ writeStdout(successLine(response2.message || "Trash emptied."));
2829
+ return;
2830
+ }
2831
+ const rawTargets = [];
2832
+ if (typeof identifiers === "string" && identifiers.trim()) {
2833
+ rawTargets.push(identifiers.trim());
2834
+ } else if (Array.isArray(identifiers)) {
2835
+ for (const item of identifiers) {
2836
+ if (typeof item === "string" && item.trim()) {
2837
+ rawTargets.push(item.trim());
2838
+ }
2839
+ }
2840
+ }
2841
+ if (options.ids) {
2842
+ for (const id of options.ids.split(",")) {
2843
+ const trimmed = id.trim();
2844
+ if (trimmed) {
2845
+ rawTargets.push(trimmed);
2846
+ }
2847
+ }
2848
+ }
2849
+ const uniqueTargets = Array.from(new Set(rawTargets));
2850
+ if (uniqueTargets.length === 0) {
2317
2851
  throw new CliError(
2318
- "PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
2852
+ "Specify one or more link identifiers or aliases to delete, or use --all to delete all links."
2319
2853
  );
2320
2854
  }
2321
- const response = await client.deleteUrl(resolvedId);
2855
+ if (uniqueTargets.length === 1) {
2856
+ const lookupResponse = await client.getUrl(uniqueTargets[0]);
2857
+ const resolvedId = getLinkId(lookupResponse.data);
2858
+ if (!resolvedId) {
2859
+ throw new CliError(
2860
+ "PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
2861
+ );
2862
+ }
2863
+ const response2 = await client.deleteUrl(resolvedId);
2864
+ if (options.json) {
2865
+ writeJson(response2);
2866
+ return;
2867
+ }
2868
+ if (options.quiet) {
2869
+ return;
2870
+ }
2871
+ writeStdout(successLine(response2.message));
2872
+ return;
2873
+ }
2874
+ const resolvedIds = [];
2875
+ for (const target of uniqueTargets) {
2876
+ try {
2877
+ const lookup = await client.getUrl(target);
2878
+ const id = getLinkId(lookup.data);
2879
+ if (id) {
2880
+ resolvedIds.push(id);
2881
+ } else {
2882
+ resolvedIds.push(target);
2883
+ }
2884
+ } catch {
2885
+ resolvedIds.push(target);
2886
+ }
2887
+ }
2888
+ const response = await client.deleteUrlsBulk(resolvedIds);
2322
2889
  if (options.json) {
2323
2890
  writeJson(response);
2324
2891
  return;
@@ -2326,7 +2893,11 @@ async function deleteLink(idOrAlias, options) {
2326
2893
  if (options.quiet) {
2327
2894
  return;
2328
2895
  }
2329
- writeStdout(successLine(response.message));
2896
+ writeStdout(
2897
+ successLine(
2898
+ response.message || `Deleted ${resolvedIds.length} short link${resolvedIds.length === 1 ? "" : "s"}.`
2899
+ )
2900
+ );
2330
2901
  }
2331
2902
 
2332
2903
  // src/commands/login.ts
@@ -2513,7 +3084,7 @@ async function deleteWebhook(id, options) {
2513
3084
  }
2514
3085
  writeStdout(successLine(response.message));
2515
3086
  }
2516
- async function listWebhookEvents(options) {
3087
+ function listWebhookEvents(options) {
2517
3088
  const response = {
2518
3089
  success: true,
2519
3090
  message: "Webhook events loaded.",
@@ -2550,6 +3121,230 @@ async function whoami(options) {
2550
3121
  writeStdout(userTable(response.data, config.apiBaseUrl));
2551
3122
  }
2552
3123
 
3124
+ // src/commands/job.ts
3125
+ async function getClient() {
3126
+ const config = await getAuthConfig(process.env);
3127
+ return new ApiClient(config);
3128
+ }
3129
+ async function listJobs(options) {
3130
+ const client = await getClient();
3131
+ const response = await client.getJobStatus();
3132
+ if (options.json) {
3133
+ writeJson(response);
3134
+ return;
3135
+ }
3136
+ if (options.quiet) {
3137
+ const ids = response.data.jobs.map((job) => job.id).join("\n");
3138
+ if (ids) {
3139
+ writeStdout(ids);
3140
+ }
3141
+ return;
3142
+ }
3143
+ writeStdout(successLine(response.message));
3144
+ writeStdout();
3145
+ writeStdout(formatJobsList(response.data));
3146
+ }
3147
+ async function getJob(id, options) {
3148
+ const client = await getClient();
3149
+ const response = await client.getJobStatus();
3150
+ const job = response.data.jobs.find((j) => j.id === id);
3151
+ if (!job) {
3152
+ throw new CliError(`Job '${id}' not found.`, 1);
3153
+ }
3154
+ if (options.json) {
3155
+ writeJson({
3156
+ success: true,
3157
+ message: "Job loaded.",
3158
+ data: job,
3159
+ timestamp: response.timestamp
3160
+ });
3161
+ return;
3162
+ }
3163
+ if (options.quiet) {
3164
+ writeStdout(job.id);
3165
+ return;
3166
+ }
3167
+ writeStdout(successLine(`Job ${job.id} loaded.`));
3168
+ writeStdout();
3169
+ writeStdout(formatJobDetails(job));
3170
+ }
3171
+ async function runJob(id, options) {
3172
+ const client = await getClient();
3173
+ const response = await client.runJob(id);
3174
+ if (options.json) {
3175
+ writeJson(response);
3176
+ return;
3177
+ }
3178
+ if (options.quiet) {
3179
+ writeStdout(response.data.status);
3180
+ return;
3181
+ }
3182
+ writeStdout(successLine(response.message));
3183
+ writeStdout();
3184
+ writeStdout(formatRunJobResult(response.data));
3185
+ }
3186
+ async function runDueJobs(options) {
3187
+ const client = await getClient();
3188
+ const response = await client.runDueJobs();
3189
+ if (options.json) {
3190
+ writeJson(response);
3191
+ return;
3192
+ }
3193
+ if (options.quiet) {
3194
+ const statuses = response.data.results.map((r) => r.status).join("\n");
3195
+ if (statuses) {
3196
+ writeStdout(statuses);
3197
+ }
3198
+ return;
3199
+ }
3200
+ writeStdout(successLine(response.message));
3201
+ writeStdout();
3202
+ writeStdout(formatRunDueResult(response.data));
3203
+ }
3204
+ async function listJobHistory(id, options) {
3205
+ const client = await getClient();
3206
+ const response = await client.getJobStatus();
3207
+ const job = response.data.jobs.find((j) => j.id === id);
3208
+ if (!job) {
3209
+ throw new CliError(`Job '${id}' not found.`, 1);
3210
+ }
3211
+ if (options.json) {
3212
+ writeJson({
3213
+ success: true,
3214
+ message: "History loaded.",
3215
+ data: job.recent_runs || [],
3216
+ timestamp: response.timestamp
3217
+ });
3218
+ return;
3219
+ }
3220
+ if (options.quiet) {
3221
+ const ids = (job.recent_runs || []).map((r) => r.id).join("\n");
3222
+ if (ids) {
3223
+ writeStdout(ids);
3224
+ }
3225
+ return;
3226
+ }
3227
+ writeStdout(successLine(`History for job ${job.id} loaded.`));
3228
+ writeStdout();
3229
+ writeStdout(formatJobHistory(job.recent_runs || []));
3230
+ }
3231
+ async function clearJobHistory(options) {
3232
+ const client = await getClient();
3233
+ const response = await client.clearJobHistory(options.job);
3234
+ if (options.json) {
3235
+ writeJson(response);
3236
+ return;
3237
+ }
3238
+ if (options.quiet) {
3239
+ return;
3240
+ }
3241
+ writeStdout(successLine(response.message));
3242
+ }
3243
+ async function updateJobSchedule(id, options) {
3244
+ const client = await getClient();
3245
+ if (options.enabled && options.disabled) {
3246
+ throw new CliError("Cannot specify both --enabled and --disabled.", 1);
3247
+ }
3248
+ const payload = {};
3249
+ if (options.interval !== void 0) {
3250
+ const interval = parseInt(options.interval, 10);
3251
+ if (isNaN(interval) || interval <= 0) {
3252
+ throw new CliError("Interval must be a positive integer.", 1);
3253
+ }
3254
+ payload.interval_seconds = interval;
3255
+ }
3256
+ if (options.preferredTime !== void 0) {
3257
+ if (options.preferredTime.toLowerCase() === "none" || options.preferredTime === "") {
3258
+ payload.preferred_run_time = null;
3259
+ } else if (/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test(options.preferredTime)) {
3260
+ payload.preferred_run_time = options.preferredTime;
3261
+ } else {
3262
+ throw new CliError(
3263
+ "Preferred time must be in HH:MM format or 'none'.",
3264
+ 1
3265
+ );
3266
+ }
3267
+ }
3268
+ if (options.enabled !== void 0) {
3269
+ payload.is_enabled = true;
3270
+ } else if (options.disabled !== void 0) {
3271
+ payload.is_enabled = false;
3272
+ }
3273
+ if (Object.keys(payload).length === 0) {
3274
+ throw new CliError("No schedule changes requested.", 1);
3275
+ }
3276
+ const response = await client.updateJobSchedule(id, payload);
3277
+ if (options.json) {
3278
+ writeJson(response);
3279
+ return;
3280
+ }
3281
+ if (options.quiet) {
3282
+ return;
3283
+ }
3284
+ writeStdout(successLine(response.message));
3285
+ writeStdout();
3286
+ writeStdout(formatJobDetails(response.data));
3287
+ }
3288
+ async function resetJobSchedule(id, options) {
3289
+ const client = await getClient();
3290
+ const response = await client.resetJobSchedule(id);
3291
+ if (options.json) {
3292
+ writeJson(response);
3293
+ return;
3294
+ }
3295
+ if (options.quiet) {
3296
+ return;
3297
+ }
3298
+ writeStdout(successLine(response.message));
3299
+ writeStdout();
3300
+ writeStdout(formatJobDetails(response.data));
3301
+ }
3302
+ async function updateJobSettings(options) {
3303
+ const client = await getClient();
3304
+ if (options.retentionDays !== void 0) {
3305
+ const days = parseInt(options.retentionDays, 10);
3306
+ if (isNaN(days) || days < 0) {
3307
+ throw new CliError(
3308
+ "Retention days must be a non-negative integer.",
3309
+ 1
3310
+ );
3311
+ }
3312
+ const response2 = await client.updateJobSettings({
3313
+ retention_days: days
3314
+ });
3315
+ if (options.json) {
3316
+ writeJson(response2);
3317
+ return;
3318
+ }
3319
+ if (options.quiet) {
3320
+ writeStdout(String(response2.data.retention_days));
3321
+ return;
3322
+ }
3323
+ writeStdout(successLine(response2.message));
3324
+ return;
3325
+ }
3326
+ const response = await client.getJobStatus();
3327
+ if (options.json) {
3328
+ writeJson({
3329
+ success: true,
3330
+ message: "Settings loaded.",
3331
+ data: {
3332
+ retention_days: response.data.retention_days,
3333
+ timezone: response.data.timezone
3334
+ },
3335
+ timestamp: response.timestamp
3336
+ });
3337
+ return;
3338
+ }
3339
+ if (options.quiet) {
3340
+ writeStdout(String(response.data.retention_days));
3341
+ return;
3342
+ }
3343
+ writeStdout(successLine("Settings loaded."));
3344
+ writeStdout(`History retention: ${response.data.retention_days} days`);
3345
+ writeStdout(`Timezone: ${response.data.timezone}`);
3346
+ }
3347
+
2553
3348
  // src/index.ts
2554
3349
  function parseNumber(label) {
2555
3350
  return (value) => {
@@ -2584,7 +3379,7 @@ function getRetryCommandName(argv) {
2584
3379
  if (!first || first.startsWith("-")) {
2585
3380
  return void 0;
2586
3381
  }
2587
- if (first === "webhook" || first === "webhooks") {
3382
+ if (first === "webhook" || first === "activity" || first === "job") {
2588
3383
  const second = argv[3]?.trim();
2589
3384
  if (second && !second.startsWith("-")) {
2590
3385
  return `${first} ${second}`;
@@ -2592,10 +3387,33 @@ function getRetryCommandName(argv) {
2592
3387
  }
2593
3388
  return first;
2594
3389
  }
3390
+ var COMMAND_SUGGESTIONS = {
3391
+ activities: "activity",
3392
+ webhooks: "webhook",
3393
+ jobs: "job",
3394
+ cron: "job",
3395
+ "scheduled-jobs": "job",
3396
+ links: "list",
3397
+ urls: "list"
3398
+ };
2595
3399
  async function main() {
2596
3400
  const program = new Command();
2597
3401
  const version = await getCliVersion();
2598
- program.name("peakurl").description("Manage your PeakURL site from the terminal.").helpOption("-h, --help", "Show help").helpCommand("help [command]", "Show help for a command").version(version, "-v, --version", "Show CLI version").showHelpAfterError().showSuggestionAfterError().addHelpText(
3402
+ program.name("peakurl").description("Manage your PeakURL site from the terminal.").helpOption("-h, --help", "Show help").helpCommand("help [command]", "Show help for a command").version(version, "-v, --version", "Show CLI version").configureOutput({
3403
+ outputError: (str, write) => {
3404
+ const match = /error: unknown command '([^']+)'/.exec(str);
3405
+ if (match && COMMAND_SUGGESTIONS[match[1]]) {
3406
+ const suggestion = COMMAND_SUGGESTIONS[match[1]];
3407
+ write(
3408
+ `error: unknown command '${match[1]}'. Did you mean 'peakurl ${suggestion}'?
3409
+
3410
+ `
3411
+ );
3412
+ return;
3413
+ }
3414
+ write(str);
3415
+ }
3416
+ }).showHelpAfterError().showSuggestionAfterError().addHelpText(
2599
3417
  "after",
2600
3418
  `
2601
3419
  Get Started:
@@ -2609,6 +3427,8 @@ Common Commands:
2609
3427
  peakurl list --limit 10
2610
3428
  peakurl import ./links.csv
2611
3429
  peakurl export --format csv
3430
+ peakurl activity list
3431
+ peakurl job list
2612
3432
  peakurl webhook list
2613
3433
  peakurl update --check
2614
3434
 
@@ -2710,8 +3530,57 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
2710
3530
  ["peakurl get docs", "peakurl get url_123 --json"]
2711
3531
  );
2712
3532
  addExamples(
2713
- program.command("delete").summary("Delete one short link").description("Delete a PeakURL short link by id or alias.").helpOption("-h, --help", "Show help").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteLink),
2714
- ["peakurl delete docs", "peakurl delete url_123 --quiet"]
3533
+ program.command("delete").summary("Delete short links").description(
3534
+ "Delete PeakURL short links by ID or alias, in bulk, or clear all links."
3535
+ ).helpOption("-h, --help", "Show help").argument("[id-or-alias...]", "Link identifier(s) or alias(es)").option("--all", "Delete all accessible short links").option(
3536
+ "--trash, --empty-trash",
3537
+ "Empty all short links currently in trash"
3538
+ ).option(
3539
+ "--ids <ids>",
3540
+ "Comma-separated list of link IDs to bulk delete"
3541
+ ).option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteLink),
3542
+ [
3543
+ "peakurl delete docs",
3544
+ "peakurl delete docs pricing launch",
3545
+ "peakurl delete --ids url_1,url_2,url_3",
3546
+ "peakurl delete --empty-trash",
3547
+ "peakurl delete --all"
3548
+ ]
3549
+ );
3550
+ const activity = program.command("activity").summary("View and manage activity logs").helpOption("-h, --help", "Show help").description(
3551
+ "View audit log activity entries, delete specific records, or clear all history."
3552
+ );
3553
+ addExamples(activity, [
3554
+ "peakurl activity list",
3555
+ "peakurl activity delete act_123 act_456",
3556
+ "peakurl activity clear"
3557
+ ]);
3558
+ addExamples(
3559
+ activity.command("list", { isDefault: true }).summary("List activity logs").description("List audit log activity records.").helpOption("-h, --help", "Show help").option("--page <number>", "Page number", parseNumber("page")).option("--limit <number>", "Page size", parseNumber("limit")).option("--search <query>", "Search term").option("--json", "Print machine-readable output").option("--quiet", "Print only activity record IDs").action(listActivity),
3560
+ [
3561
+ "peakurl activity",
3562
+ "peakurl activity list",
3563
+ "peakurl activity list --limit 25 --page 1",
3564
+ "peakurl activity list --search delete --json"
3565
+ ]
3566
+ );
3567
+ addExamples(
3568
+ activity.command("delete").summary("Delete activity logs").description(
3569
+ "Delete one or more activity logs by ID, or clear all logs with --all."
3570
+ ).helpOption("-h, --help", "Show help").argument("[ids...]", "One or more activity log IDs to delete").option("--all", "Delete all activity log history").option(
3571
+ "--ids <ids>",
3572
+ "Comma-separated list of activity log IDs to delete"
3573
+ ).option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteActivity),
3574
+ [
3575
+ "peakurl activity delete act_123",
3576
+ "peakurl activity delete act_123 act_456",
3577
+ "peakurl activity delete --ids act_1,act_2",
3578
+ "peakurl activity delete --all"
3579
+ ]
3580
+ );
3581
+ addExamples(
3582
+ activity.command("clear").summary("Clear all activity logs").description("Delete all audit log activity records permanently.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(clearActivity),
3583
+ ["peakurl activity clear", "peakurl activity clear --json"]
2715
3584
  );
2716
3585
  addExamples(
2717
3586
  program.command("update").summary("Check for CLI updates").description(
@@ -2719,18 +3588,110 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
2719
3588
  ).helpOption("-h, --help", "Show help").option(
2720
3589
  "--check",
2721
3590
  "Alias for checking update status without changing anything"
2722
- ).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action((options) => checkUpdate(options, version)),
3591
+ ).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action(
3592
+ (options) => checkUpdate(options, version)
3593
+ ),
2723
3594
  ["peakurl update", "peakurl update --check", "peakurl update --json"]
2724
3595
  );
2725
- const webhook = program.command("webhook").alias("webhooks").summary("Manage webhooks").helpOption("-h, --help", "Show help").description("Manage outbound webhook integrations.");
3596
+ const jobCmd = program.command("job").summary("Manage scheduled jobs").description(
3597
+ "Manage server-side scheduled jobs, view their execution history, and run them manually."
3598
+ ).helpOption("-h, --help", "Show help");
3599
+ addExamples(jobCmd, [
3600
+ "peakurl job",
3601
+ "peakurl job list",
3602
+ "peakurl job get peakurl_version_check",
3603
+ "peakurl job run peakurl_version_check",
3604
+ "peakurl job run-due"
3605
+ ]);
3606
+ addExamples(
3607
+ jobCmd.command("list", { isDefault: true }).summary("List scheduled jobs").description("List all registered scheduled jobs.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Print only job IDs").action(listJobs),
3608
+ [
3609
+ "peakurl job",
3610
+ "peakurl job list",
3611
+ "peakurl job list --json",
3612
+ "peakurl job list --quiet"
3613
+ ]
3614
+ );
3615
+ addExamples(
3616
+ jobCmd.command("get").summary("Show job details").description(
3617
+ "Show detailed configuration and status for one scheduled job."
3618
+ ).helpOption("-h, --help", "Show help").argument("<id>", "Job identifier").option("--json", "Print machine-readable output").option("--quiet", "Print only the job ID").action(getJob),
3619
+ [
3620
+ "peakurl job get peakurl_version_check",
3621
+ "peakurl job get peakurl_version_check --json"
3622
+ ]
3623
+ );
3624
+ addExamples(
3625
+ jobCmd.command("run").summary("Run a scheduled job").description("Force a specific scheduled job to run immediately.").helpOption("-h, --help", "Show help").argument("<id>", "Job identifier").option("--json", "Print machine-readable output").option("--quiet", "Print only the execution status").action(runJob),
3626
+ [
3627
+ "peakurl job run peakurl_version_check",
3628
+ "peakurl job run peakurl_version_check --json"
3629
+ ]
3630
+ );
3631
+ addExamples(
3632
+ jobCmd.command("run-due").summary("Run due jobs").description("Trigger all scheduled jobs that are currently due.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Print only the execution statuses").action(runDueJobs),
3633
+ ["peakurl job run-due", "peakurl job run-due --json"]
3634
+ );
3635
+ addExamples(
3636
+ jobCmd.command("history").summary("View job history").description("View recent execution history for a scheduled job.").helpOption("-h, --help", "Show help").argument("<id>", "Job identifier").option("--json", "Print machine-readable output").option("--quiet", "Print only history record IDs").action(listJobHistory),
3637
+ [
3638
+ "peakurl job history peakurl_version_check",
3639
+ "peakurl job history peakurl_version_check --json"
3640
+ ]
3641
+ );
3642
+ addExamples(
3643
+ jobCmd.command("clear-history").summary("Clear job history").description(
3644
+ "Clear execution history for all jobs or a specific job."
3645
+ ).helpOption("-h, --help", "Show help").option("--job <id>", "Specific job identifier to clear").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(clearJobHistory),
3646
+ [
3647
+ "peakurl job clear-history",
3648
+ "peakurl job clear-history --job peakurl_version_check",
3649
+ "peakurl job clear-history --json"
3650
+ ]
3651
+ );
3652
+ addExamples(
3653
+ jobCmd.command("schedule").summary("Update job schedule").description("Update the schedule configuration for a job.").helpOption("-h, --help", "Show help").argument("<id>", "Job identifier").option("--interval <seconds>", "Execution interval in seconds").option(
3654
+ "--preferred-time <time>",
3655
+ "Preferred run time (HH:MM or 'none')"
3656
+ ).option("--enabled", "Enable the job").option("--disabled", "Disable the job").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(updateJobSchedule),
3657
+ [
3658
+ "peakurl job schedule peakurl_version_check --interval 43200",
3659
+ "peakurl job schedule peakurl_version_check --preferred-time 03:00",
3660
+ "peakurl job schedule peakurl_version_check --disabled"
3661
+ ]
3662
+ );
3663
+ addExamples(
3664
+ jobCmd.command("reset").summary("Reset job schedule").description("Reset a job's schedule to its default configuration.").helpOption("-h, --help", "Show help").argument("<id>", "Job identifier").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(resetJobSchedule),
3665
+ [
3666
+ "peakurl job reset peakurl_version_check",
3667
+ "peakurl job reset peakurl_version_check --json"
3668
+ ]
3669
+ );
3670
+ addExamples(
3671
+ jobCmd.command("settings").summary("Manage scheduler settings").description("View or update global scheduler settings.").helpOption("-h, --help", "Show help").option(
3672
+ "--retention-days <days>",
3673
+ "Number of days to keep execution history"
3674
+ ).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action(updateJobSettings),
3675
+ [
3676
+ "peakurl job settings",
3677
+ "peakurl job settings --retention-days 14",
3678
+ "peakurl job settings --json"
3679
+ ]
3680
+ );
3681
+ const webhook = program.command("webhook").summary("Manage webhooks").helpOption("-h, --help", "Show help").description("Manage outbound webhook integrations.");
2726
3682
  addExamples(webhook, [
3683
+ "peakurl webhook",
2727
3684
  "peakurl webhook list",
2728
3685
  "peakurl webhook create https://example.com/api/webhooks/peakurl --event link.clicked",
2729
3686
  "peakurl webhook events"
2730
3687
  ]);
2731
3688
  addExamples(
2732
- webhook.command("list").summary("List webhooks").description("List outbound webhooks.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Print minimal webhook identifiers").action(listWebhooks),
2733
- ["peakurl webhook list", "peakurl webhook list --json"]
3689
+ webhook.command("list", { isDefault: true }).summary("List webhooks").description("List outbound webhooks.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Print minimal webhook identifiers").action(listWebhooks),
3690
+ [
3691
+ "peakurl webhook",
3692
+ "peakurl webhook list",
3693
+ "peakurl webhook list --json"
3694
+ ]
2734
3695
  );
2735
3696
  addExamples(
2736
3697
  webhook.command("create").summary("Create a webhook").description("Create an outbound webhook.").helpOption("-h, --help", "Show help").argument("<url>", "Webhook endpoint URL").option(