peakurl 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -6,7 +6,7 @@ The official command-line interface for PeakURL.
6
6
 
7
7
  Use `peakurl` to create short links, inspect existing links, and manage your PeakURL account from the terminal.
8
8
 
9
- Learn more in the full CLI docs: <https://peakurl.org/docs/cli>
9
+ Learn more in the full CLI docs [here.](https://go.peakurl.org/2aae02)
10
10
 
11
11
  ## Install
12
12
 
@@ -66,6 +66,7 @@ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
66
66
  | `peakurl status` | Show the current system status snapshot for the site. |
67
67
  | `peakurl core download` | Download and extract the latest PeakURL core package. |
68
68
  | `peakurl create <url>` | Create a new short link. |
69
+ | `peakurl edit <id-or-alias>` | Update an existing link's destination URL or metadata. |
69
70
  | `peakurl import <file>` | Import links from a local CSV, JSON, or XML file. |
70
71
  | `peakurl export` | Export accessible links as CSV, JSON, or XML. |
71
72
  | `peakurl list` | List links in your account. |
@@ -73,6 +74,7 @@ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
73
74
  | `peakurl delete [id-or-alias...]` | Delete links by ID or alias, in bulk, or clear all links. |
74
75
  | `peakurl activity <subcommand>` | View audit logs, delete activity records, or clear history. |
75
76
  | `peakurl webhook <subcommand>` | List, create, delete, and inspect supported webhook events. |
77
+ | `peakurl job <cmd>` | Manage server-side scheduled jobs. |
76
78
  | `peakurl update` | Show the latest available CLI version and install command. |
77
79
 
78
80
  ## Examples
@@ -83,9 +85,24 @@ Create a short link:
83
85
  peakurl create \
84
86
  https://example.com \
85
87
  --alias example \
86
- --title "Example"
88
+ --title "Example" \
89
+ --social-title "Example — Official Site" \
90
+ --social-description "Explore the Example platform." \
91
+ --social-image-url https://example.com/og.png
87
92
  ```
88
93
 
94
+ Edit an existing short link:
95
+
96
+ ```bash
97
+ peakurl edit example \
98
+ --url https://example.com/updated-page \
99
+ --social-title "Example — Updated Preview" \
100
+ --social-description "Updated Open Graph description." \
101
+ --social-image-url https://example.com/new-og.png
102
+ ```
103
+
104
+ You can update a link's destination URL (`--url`), title, status, expiration, password, and social preview fields.
105
+
89
106
  List links as JSON:
90
107
 
91
108
  ```bash
@@ -228,6 +245,19 @@ Check the latest available CLI version:
228
245
  peakurl update --check
229
246
  ```
230
247
 
248
+ Manage scheduled jobs:
249
+
250
+ ```bash
251
+ # List all registered scheduled jobs
252
+ peakurl job list
253
+
254
+ # Force a job to run immediately
255
+ peakurl job run peakurl_version_check
256
+
257
+ # Trigger all currently due jobs
258
+ peakurl job run-due
259
+ ```
260
+
231
261
  Show the recommended install command:
232
262
 
233
263
  ```bash
@@ -254,8 +284,8 @@ export PEAKURL_DISABLE_UPDATE_CHECK=1
254
284
 
255
285
  ## Links
256
286
 
257
- - Website: <https://peakurl.org/>
258
- - CLI docs: <https://peakurl.org/docs/cli>
259
- - API docs: <https://peakurl.org/docs/api>
260
- - npm package: <https://www.npmjs.com/package/peakurl>
261
- - Issues: <https://github.com/PeakURL/CLI/issues>
287
+ - [Website](https://peakurl.org/)
288
+ - [CLI docs](https://go.peakurl.org/2aae02)
289
+ - [API docs](https://go.peakurl.org/d373f6)
290
+ - [npm package](https://go.peakurl.org/cli)
291
+ - [Issues](https://github.com/PeakURL/CLI/issues)
package/bin/peakurl.js CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFile as readFile3 } from "fs/promises";
4
+ import { readFile as readFile4 } from "fs/promises";
5
5
  import { Command, CommanderError, InvalidArgumentError } from "commander";
6
6
 
7
+ // src/api/client.ts
8
+ import { readFile, stat } from "fs/promises";
9
+ import { basename, extname } from "path";
10
+
7
11
  // src/lib/errors.ts
8
12
  var CliError = class extends Error {
9
13
  exitCode;
@@ -100,6 +104,54 @@ function normalizeWebhookUrl(value) {
100
104
  }
101
105
 
102
106
  // src/api/client.ts
107
+ var IMAGE_MIME_BY_EXTENSION = {
108
+ ".jpg": "image/jpeg",
109
+ ".jpeg": "image/jpeg",
110
+ ".png": "image/png",
111
+ ".webp": "image/webp",
112
+ ".gif": "image/gif"
113
+ };
114
+ var MAX_SOCIAL_IMAGE_BYTES = 5 * 1024 * 1024;
115
+ async function toFormData(fields, socialImagePath) {
116
+ const ext = extname(socialImagePath).toLowerCase();
117
+ const mimeType = IMAGE_MIME_BY_EXTENSION[ext];
118
+ if (!mimeType) {
119
+ throw new CliError(
120
+ "Invalid social image file type. Only JPG, PNG, WEBP, and GIF images are allowed."
121
+ );
122
+ }
123
+ let fileStats;
124
+ try {
125
+ fileStats = await stat(socialImagePath);
126
+ } catch (error) {
127
+ throw new CliError(
128
+ `Could not read social image file ${socialImagePath}.`,
129
+ 1,
130
+ {
131
+ cause: error instanceof Error ? error : void 0
132
+ }
133
+ );
134
+ }
135
+ if (fileStats.size > MAX_SOCIAL_IMAGE_BYTES) {
136
+ throw new CliError(
137
+ "Social image file is too large. Maximum size is 5MB."
138
+ );
139
+ }
140
+ const buffer = await readFile(socialImagePath);
141
+ const formData = new FormData();
142
+ for (const [key, value] of Object.entries(fields)) {
143
+ if (key === "socialImagePath" || typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
144
+ continue;
145
+ }
146
+ formData.append(key, String(value));
147
+ }
148
+ formData.append(
149
+ "socialImage",
150
+ new Blob([buffer], { type: mimeType }),
151
+ basename(socialImagePath)
152
+ );
153
+ return formData;
154
+ }
103
155
  function isApiResponse(value) {
104
156
  return Boolean(
105
157
  value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
@@ -120,7 +172,6 @@ var ApiClient = class {
120
172
  constructor(config) {
121
173
  this.config = config;
122
174
  }
123
- config;
124
175
  /**
125
176
  * Loads the currently authenticated user.
126
177
  *
@@ -146,8 +197,38 @@ var ApiClient = class {
146
197
  * @param payload Request body accepted by `POST /api/v1/urls`.
147
198
  * @returns API response envelope containing the created link.
148
199
  */
149
- createUrl(payload) {
150
- return this.request("POST", "urls", payload);
200
+ async createUrl(payload) {
201
+ if (payload.socialImagePath) {
202
+ const formData = await toFormData(
203
+ payload,
204
+ payload.socialImagePath
205
+ );
206
+ return this.request("POST", "urls", formData);
207
+ }
208
+ const { socialImagePath: _unused, ...jsonPayload } = payload;
209
+ return this.request("POST", "urls", jsonPayload);
210
+ }
211
+ /**
212
+ * Updates an existing short URL by its stable row ID.
213
+ *
214
+ * Sends `POST /api/v1/urls/{id}` with `multipart/form-data` when uploading a
215
+ * local social preview image, or `PUT /api/v1/urls/{id}` with JSON otherwise.
216
+ *
217
+ * @param id Stable link row ID.
218
+ * @param payload Fields to update on the short link.
219
+ * @returns API response envelope containing the updated link.
220
+ */
221
+ async updateUrl(id, payload) {
222
+ const path = `urls/${encodeURIComponent(id)}`;
223
+ if (payload.socialImagePath) {
224
+ const formData = await toFormData(
225
+ payload,
226
+ payload.socialImagePath
227
+ );
228
+ return this.request("POST", path, formData);
229
+ }
230
+ const { socialImagePath: _unused, ...jsonPayload } = payload;
231
+ return this.request("PUT", path, jsonPayload);
151
232
  }
152
233
  /**
153
234
  * Lists short URLs with optional pagination and filtering.
@@ -314,6 +395,86 @@ var ApiClient = class {
314
395
  `webhooks/${encodeURIComponent(id)}`
315
396
  );
316
397
  }
398
+ /**
399
+ * Loads the current status of the cron scheduler.
400
+ *
401
+ * @returns API response envelope containing the scheduler status.
402
+ */
403
+ getJobStatus() {
404
+ return this.request("GET", "system/cron");
405
+ }
406
+ /**
407
+ * Triggers all due cron jobs to run.
408
+ *
409
+ * @returns API response envelope containing the execution results.
410
+ */
411
+ runDueJobs() {
412
+ return this.request("POST", "system/cron/run");
413
+ }
414
+ /**
415
+ * Forces a specific cron job to run immediately.
416
+ *
417
+ * @param id Job identifier.
418
+ * @returns API response envelope containing the execution result.
419
+ */
420
+ runJob(id) {
421
+ return this.request(
422
+ "POST",
423
+ `system/cron/run/${encodeURIComponent(id)}`
424
+ );
425
+ }
426
+ /**
427
+ * Clears cron execution history.
428
+ *
429
+ * @param jobId Optional job identifier to clear history only for one job.
430
+ * @returns API response envelope containing the deleted count.
431
+ */
432
+ clearJobHistory(jobId) {
433
+ return this.request(
434
+ "POST",
435
+ "system/cron/history/clear",
436
+ jobId ? { job_id: jobId } : void 0
437
+ );
438
+ }
439
+ /**
440
+ * Updates the schedule configuration for a cron job.
441
+ *
442
+ * @param id Job identifier.
443
+ * @param payload New configuration options.
444
+ * @returns API response envelope containing the updated job.
445
+ */
446
+ updateJobSchedule(id, payload) {
447
+ return this.request(
448
+ "PATCH",
449
+ `system/cron/jobs/${encodeURIComponent(id)}`,
450
+ payload
451
+ );
452
+ }
453
+ /**
454
+ * Resets a cron job schedule to its default configuration.
455
+ *
456
+ * @param id Job identifier.
457
+ * @returns API response envelope containing the restored job.
458
+ */
459
+ resetJobSchedule(id) {
460
+ return this.request(
461
+ "POST",
462
+ `system/cron/jobs/${encodeURIComponent(id)}/reset`
463
+ );
464
+ }
465
+ /**
466
+ * Updates the global cron settings, such as history retention.
467
+ *
468
+ * @param payload New global settings.
469
+ * @returns API response envelope containing the updated retention settings.
470
+ */
471
+ updateJobSettings(payload) {
472
+ return this.request(
473
+ "POST",
474
+ "system/cron/settings",
475
+ payload
476
+ );
477
+ }
317
478
  /**
318
479
  * Performs one authenticated API request and normalizes the response.
319
480
  *
@@ -327,15 +488,16 @@ var ApiClient = class {
327
488
  async request(method, path, body, query) {
328
489
  const url = buildApiUrl(this.config.apiBaseUrl, path, query);
329
490
  let response;
491
+ const isFormData = body instanceof FormData;
330
492
  try {
331
493
  response = await fetch(url, {
332
494
  method,
333
495
  headers: {
334
496
  Accept: "application/json",
335
497
  Authorization: `Bearer ${this.config.apiKey}`,
336
- ...body ? { "Content-Type": "application/json" } : {}
498
+ ...body && !isFormData ? { "Content-Type": "application/json" } : {}
337
499
  },
338
- body: body ? JSON.stringify(body) : void 0
500
+ body: isFormData ? body : body ? JSON.stringify(body) : void 0
339
501
  });
340
502
  } catch (error) {
341
503
  throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
@@ -613,7 +775,7 @@ function formatActivitySummary(data, count) {
613
775
  }
614
776
 
615
777
  // src/config/store.ts
616
- import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
778
+ import { chmod, mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
617
779
  import { dirname, join } from "path";
618
780
  import envPaths from "env-paths";
619
781
  var CONFIG_FILENAME = "config.json";
@@ -652,7 +814,7 @@ var ConfigStore = class {
652
814
  */
653
815
  async load() {
654
816
  try {
655
- const content = await readFile(this.filePath, "utf8");
817
+ const content = await readFile2(this.filePath, "utf8");
656
818
  const parsed = JSON.parse(content);
657
819
  const apiBaseUrl = typeof parsed?.apiBaseUrl === "string" ? parsed.apiBaseUrl : typeof parsed?.baseUrl === "string" ? parsed.baseUrl : void 0;
658
820
  if (typeof apiBaseUrl !== "string" || typeof parsed?.apiKey !== "string") {
@@ -742,7 +904,7 @@ var StateStore = class {
742
904
  */
743
905
  async load() {
744
906
  try {
745
- const content = await readFile(this.filePath, "utf8");
907
+ const content = await readFile2(this.filePath, "utf8");
746
908
  const parsed = JSON.parse(content);
747
909
  return parsed && typeof parsed === "object" ? parsed : {};
748
910
  } catch {
@@ -1161,7 +1323,10 @@ var EXPORT_HEADERS = [
1161
1323
  "short_url",
1162
1324
  "clicks",
1163
1325
  "unique_clicks",
1164
- "created_at"
1326
+ "created_at",
1327
+ "social_title",
1328
+ "social_description",
1329
+ "social_image_url"
1165
1330
  ];
1166
1331
  function text(value) {
1167
1332
  if (typeof value === "string") {
@@ -1220,7 +1385,10 @@ function buildExportRows(links) {
1220
1385
  short_url: text(link.shortUrl),
1221
1386
  clicks: typeof link.clicks === "number" ? link.clicks : "",
1222
1387
  unique_clicks: typeof link.uniqueClicks === "number" ? link.uniqueClicks : "",
1223
- created_at: text(link.createdAt)
1388
+ created_at: text(link.createdAt),
1389
+ social_title: text(link.socialTitle) || text(link.socialPreview?.title),
1390
+ social_description: text(link.socialDescription) || text(link.socialPreview?.description),
1391
+ social_image_url: text(link.socialImageUrl) || text(link.socialPreview?.externalImageUrl) || text(link.socialPreview?.imageUrl)
1224
1392
  }));
1225
1393
  }
1226
1394
  function serializeLinkExport(links, format) {
@@ -1240,6 +1408,9 @@ function serializeLinkExport(links, format) {
1240
1408
  <clicks>${xmlValue(row2.clicks)}</clicks>
1241
1409
  <uniqueClicks>${xmlValue(row2.unique_clicks)}</uniqueClicks>
1242
1410
  <createdAt>${xmlValue(row2.created_at)}</createdAt>
1411
+ <socialTitle>${xmlValue(row2.social_title)}</socialTitle>
1412
+ <socialDescription>${xmlValue(row2.social_description)}</socialDescription>
1413
+ <socialImageUrl>${xmlValue(row2.social_image_url)}</socialImageUrl>
1243
1414
  </url>`
1244
1415
  ).join("\n");
1245
1416
  return `<urls>
@@ -1258,7 +1429,7 @@ ${body}
1258
1429
  }
1259
1430
 
1260
1431
  // src/lib/imports.ts
1261
- import { readFile as readFile2 } from "fs/promises";
1432
+ import { readFile as readFile3 } from "fs/promises";
1262
1433
  function text2(value) {
1263
1434
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1264
1435
  }
@@ -1345,7 +1516,16 @@ function normalizeImportRow(value) {
1345
1516
  ...text2(value.utmMedium) ? { utmMedium: text2(value.utmMedium) } : {},
1346
1517
  ...text2(value.utmCampaign) ? { utmCampaign: text2(value.utmCampaign) } : {},
1347
1518
  ...text2(value.utmTerm) ? { utmTerm: text2(value.utmTerm) } : {},
1348
- ...text2(value.utmContent) ? { utmContent: text2(value.utmContent) } : {}
1519
+ ...text2(value.utmContent) ? { utmContent: text2(value.utmContent) } : {},
1520
+ ...text2(value.socialTitle) || text2(value.social_title) ? {
1521
+ socialTitle: text2(value.socialTitle) || text2(value.social_title)
1522
+ } : {},
1523
+ ...text2(value.socialDescription) || text2(value.social_description) ? {
1524
+ socialDescription: text2(value.socialDescription) || text2(value.social_description)
1525
+ } : {},
1526
+ ...text2(value.socialImageUrl) || text2(value.social_image_url) ? {
1527
+ socialImageUrl: text2(value.socialImageUrl) || text2(value.social_image_url)
1528
+ } : {}
1349
1529
  };
1350
1530
  }
1351
1531
  function parseJson(text6) {
@@ -1416,6 +1596,18 @@ function parseCsv(text6) {
1416
1596
  }
1417
1597
  if (header === "utmcontent") {
1418
1598
  entry.utmContent = value;
1599
+ return;
1600
+ }
1601
+ if (header === "socialtitle") {
1602
+ entry.socialTitle = value;
1603
+ return;
1604
+ }
1605
+ if (header === "socialdescription") {
1606
+ entry.socialDescription = value;
1607
+ return;
1608
+ }
1609
+ if (header === "socialimageurl" || header === "socialimage") {
1610
+ entry.socialImageUrl = value;
1419
1611
  }
1420
1612
  });
1421
1613
  const link = normalizeImportRow(entry);
@@ -1453,7 +1645,10 @@ function parseXml(text6) {
1453
1645
  utmMedium: getValue("utmMedium"),
1454
1646
  utmCampaign: getValue("utmCampaign"),
1455
1647
  utmTerm: getValue("utmTerm"),
1456
- utmContent: getValue("utmContent")
1648
+ utmContent: getValue("utmContent"),
1649
+ socialTitle: getValue("socialTitle"),
1650
+ socialDescription: getValue("socialDescription"),
1651
+ socialImageUrl: getValue("socialImageUrl")
1457
1652
  });
1458
1653
  }).filter((item) => Boolean(item));
1459
1654
  }
@@ -1472,7 +1667,7 @@ function getImportFormat(filePath, value) {
1472
1667
  async function readImportRows(filePath, format) {
1473
1668
  let textContent;
1474
1669
  try {
1475
- textContent = await readFile2(filePath, "utf8");
1670
+ textContent = await readFile3(filePath, "utf8");
1476
1671
  } catch (error) {
1477
1672
  throw new CliError(`Could not read import file ${filePath}.`, 1, {
1478
1673
  cause: error instanceof Error ? error : void 0
@@ -1593,6 +1788,10 @@ function getQuietLinkValue(link) {
1593
1788
  return getLinkShortUrl(link) || getLinkAlias(link) || getLinkId(link) || "";
1594
1789
  }
1595
1790
  function formatLinkDetails(link) {
1791
+ const socialPreview = asObject2(link.socialPreview);
1792
+ const socialTitle = asString3(link.socialTitle) || asString3(socialPreview?.title);
1793
+ const socialDescription = asString3(link.socialDescription) || asString3(socialPreview?.description);
1794
+ const socialImage = asString3(link.socialImageUrl) || asString3(socialPreview?.externalImageUrl) || asString3(socialPreview?.imageUrl);
1596
1795
  const rows2 = [
1597
1796
  ["ID", getLinkId(link)],
1598
1797
  ["Alias", getLinkAlias(link)],
@@ -1600,6 +1799,9 @@ function formatLinkDetails(link) {
1600
1799
  ["Destination", getLinkDestination(link)],
1601
1800
  ["Title", asString3(link.title)],
1602
1801
  ["Status", asString3(link.status)],
1802
+ ["Social Title", socialTitle],
1803
+ ["Social Description", socialDescription],
1804
+ ["Social Image", socialImage],
1603
1805
  [
1604
1806
  "Clicks",
1605
1807
  asNumber2(link.clicks) === void 0 ? void 0 : String(link.clicks)
@@ -1985,6 +2187,121 @@ ${checks}` : void 0,
1985
2187
  return sections.length > 0 ? sections.join("\n\n") : "No system status fields returned.";
1986
2188
  }
1987
2189
 
2190
+ // src/lib/job.ts
2191
+ function formatInterval(seconds) {
2192
+ if (seconds < 60) return `${seconds}s`;
2193
+ const minutes = Math.floor(seconds / 60);
2194
+ if (minutes < 60) return `${minutes}m`;
2195
+ const hours = Math.floor(minutes / 60);
2196
+ if (hours < 24) return `${hours}h`;
2197
+ const days = Math.floor(hours / 24);
2198
+ return `${days}d`;
2199
+ }
2200
+ function formatDate(date) {
2201
+ if (!date) return "never";
2202
+ return new Date(date).toISOString().replace("T", " ").substring(0, 19);
2203
+ }
2204
+ function formatJobsList(status2) {
2205
+ const rows2 = status2.jobs.map((job) => [
2206
+ job.id,
2207
+ job.title,
2208
+ job.status,
2209
+ job.is_enabled ? "yes" : "no",
2210
+ formatDate(job.next_run_at)
2211
+ ]);
2212
+ const table = formatTable(
2213
+ ["ID", "Job", "Status", "Enabled", "Next Run"],
2214
+ rows2
2215
+ );
2216
+ const summary = `
2217
+ ${status2.jobs_count} jobs registered.
2218
+ Timezone: ${status2.timezone}
2219
+ History retention: ${status2.retention_days} days`;
2220
+ return `${table}${summary}`;
2221
+ }
2222
+ function formatJobDetails(job) {
2223
+ const rows2 = [
2224
+ ["ID", job.id],
2225
+ ["Title", job.title],
2226
+ ["Status", job.status],
2227
+ ["Enabled", job.is_enabled ? "yes" : "no"],
2228
+ ["Current interval", formatInterval(job.interval_seconds)],
2229
+ [
2230
+ "Recommended interval",
2231
+ formatInterval(job.recommended_interval_seconds)
2232
+ ],
2233
+ ["Preferred run time", job.preferred_run_time || "none"],
2234
+ ["Customized", job.is_customized ? "yes" : "no"],
2235
+ ["Next run", formatDate(job.next_run_at)],
2236
+ ["Last run", formatDate(job.last_run_at)],
2237
+ ["Last finished", formatDate(job.last_finished_at)],
2238
+ ["Attempts", String(job.attempts)],
2239
+ ["Maximum attempts", String(job.max_attempts)],
2240
+ ["Last error", job.last_error || "none"]
2241
+ ];
2242
+ let out = formatDetailsTable(rows2);
2243
+ if (job.recent_runs && job.recent_runs.length > 0) {
2244
+ out += `
2245
+
2246
+ Recent Runs:
2247
+ ${formatJobHistory(job.recent_runs)}`;
2248
+ }
2249
+ return out;
2250
+ }
2251
+ function formatJobHistory(runs) {
2252
+ if (!runs || runs.length === 0) {
2253
+ return "No recent runs.";
2254
+ }
2255
+ const rows2 = runs.map((run) => [
2256
+ run.id,
2257
+ run.status,
2258
+ String(run.attempt),
2259
+ formatDate(run.started_at),
2260
+ formatDate(run.finished_at),
2261
+ run.duration_ms ? `${run.duration_ms}ms` : "-",
2262
+ run.output_summary || "-",
2263
+ run.error_message || "-"
2264
+ ]);
2265
+ return formatTable(
2266
+ [
2267
+ "Run ID",
2268
+ "Status",
2269
+ "Attempt",
2270
+ "Started",
2271
+ "Finished",
2272
+ "Duration",
2273
+ "Summary",
2274
+ "Error"
2275
+ ],
2276
+ rows2
2277
+ );
2278
+ }
2279
+ function formatRunJobResult(result) {
2280
+ let out = `Job: ${result.job_id}
2281
+ Status: ${result.status}`;
2282
+ if (result.summary) {
2283
+ out += `
2284
+ Summary: ${result.summary}`;
2285
+ }
2286
+ if (result.error) {
2287
+ out += `
2288
+ Error: ${result.error}`;
2289
+ }
2290
+ return out;
2291
+ }
2292
+ function formatRunDueResult(result) {
2293
+ if (!result.results || result.results.length === 0) {
2294
+ return "No jobs were due.";
2295
+ }
2296
+ const rows2 = result.results.map((r) => [
2297
+ r.job_id,
2298
+ r.status,
2299
+ r.summary || "-",
2300
+ r.error || "-"
2301
+ ]);
2302
+ return formatTable(["Job ID", "Status", "Summary", "Error"], rows2);
2303
+ }
2304
+
1988
2305
  // src/lib/update.ts
1989
2306
  var PACKAGE_NAME = "peakurl";
1990
2307
  var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
@@ -2473,6 +2790,11 @@ function normalizeExpiresAt(value) {
2473
2790
  return value;
2474
2791
  }
2475
2792
  async function createLink(destinationUrl, options) {
2793
+ if (options.socialImage && options.socialImageUrl) {
2794
+ throw new CliError(
2795
+ "Use either --social-image or --social-image-url, not both."
2796
+ );
2797
+ }
2476
2798
  const config = await getAuthConfig(process.env);
2477
2799
  const response = await new ApiClient(config).createUrl({
2478
2800
  destinationUrl: normalizeDestinationUrl(destinationUrl),
@@ -2485,7 +2807,15 @@ async function createLink(destinationUrl, options) {
2485
2807
  ...options.utmMedium ? { utmMedium: options.utmMedium } : {},
2486
2808
  ...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
2487
2809
  ...options.utmTerm ? { utmTerm: options.utmTerm } : {},
2488
- ...options.utmContent ? { utmContent: options.utmContent } : {}
2810
+ ...options.utmContent ? { utmContent: options.utmContent } : {},
2811
+ ...options.socialTitle ? { socialTitle: options.socialTitle } : {},
2812
+ ...options.socialDescription ? { socialDescription: options.socialDescription } : {},
2813
+ ...options.socialImageUrl ? {
2814
+ socialImageUrl: normalizeDestinationUrl(
2815
+ options.socialImageUrl
2816
+ )
2817
+ } : {},
2818
+ ...options.socialImage ? { socialImagePath: resolve2(options.socialImage) } : {}
2489
2819
  });
2490
2820
  if (options.json) {
2491
2821
  writeJson(response);
@@ -2498,6 +2828,67 @@ async function createLink(destinationUrl, options) {
2498
2828
  writeStdout(successLine(response.message));
2499
2829
  writeStdout(formatLinkDetails(response.data));
2500
2830
  }
2831
+ async function editLink(idOrAlias, options) {
2832
+ if (options.socialImage && options.socialImageUrl) {
2833
+ throw new CliError(
2834
+ "Use either --social-image or --social-image-url, not both."
2835
+ );
2836
+ }
2837
+ if (options.removeSocialImage && (options.socialImage || options.socialImageUrl)) {
2838
+ throw new CliError(
2839
+ "Cannot combine --remove-social-image with --social-image or --social-image-url."
2840
+ );
2841
+ }
2842
+ if (options.password !== void 0 && options.clearPassword) {
2843
+ throw new CliError(
2844
+ "Use either --password or --clear-password, not both."
2845
+ );
2846
+ }
2847
+ if (options.expiresAt !== void 0 && options.clearExpiresAt) {
2848
+ throw new CliError(
2849
+ "Use either --expires-at or --clear-expires-at, not both."
2850
+ );
2851
+ }
2852
+ const payload = {
2853
+ ...options.url !== void 0 ? { destinationUrl: normalizeDestinationUrl(options.url) } : {},
2854
+ ...options.title !== void 0 ? { title: options.title } : {},
2855
+ ...options.clearPassword ? { clearPassword: true } : options.password !== void 0 ? { password: options.password } : {},
2856
+ ...options.status !== void 0 ? { status: options.status } : {},
2857
+ ...options.clearExpiresAt ? { expiresAt: "" } : options.expiresAt !== void 0 ? { expiresAt: normalizeExpiresAt(options.expiresAt) } : {},
2858
+ ...options.socialTitle !== void 0 ? { socialTitle: options.socialTitle } : {},
2859
+ ...options.socialDescription !== void 0 ? { socialDescription: options.socialDescription } : {},
2860
+ ...options.socialImageUrl !== void 0 ? {
2861
+ socialImageUrl: options.socialImageUrl ? normalizeDestinationUrl(options.socialImageUrl) : ""
2862
+ } : {},
2863
+ ...options.socialImage ? { socialImagePath: resolve2(options.socialImage) } : {},
2864
+ ...options.removeSocialImage ? { removeSocialImage: true } : {}
2865
+ };
2866
+ if (Object.keys(payload).length === 0) {
2867
+ throw new CliError(
2868
+ "Specify at least one field to update (for example --title, --url, --social-title, --social-description, or --social-image-url)."
2869
+ );
2870
+ }
2871
+ const config = await getAuthConfig(process.env);
2872
+ const client = new ApiClient(config);
2873
+ const lookupResponse = await client.getUrl(idOrAlias.trim());
2874
+ const resolvedId = getLinkId(lookupResponse.data);
2875
+ if (!resolvedId) {
2876
+ throw new CliError(
2877
+ "PeakURL returned a link record without an ID, so the CLI cannot update it safely."
2878
+ );
2879
+ }
2880
+ const response = await client.updateUrl(resolvedId, payload);
2881
+ if (options.json) {
2882
+ writeJson(response);
2883
+ return;
2884
+ }
2885
+ if (options.quiet) {
2886
+ writeStdout(getQuietLinkValue(response.data));
2887
+ return;
2888
+ }
2889
+ writeStdout(successLine(response.message || "Short URL updated."));
2890
+ writeStdout(formatLinkDetails(response.data));
2891
+ }
2501
2892
  async function importLinks(filePath, options) {
2502
2893
  const format = getImportFormat(filePath, options.format);
2503
2894
  const urls = await readImportRows(filePath, format);
@@ -2927,6 +3318,230 @@ async function whoami(options) {
2927
3318
  writeStdout(userTable(response.data, config.apiBaseUrl));
2928
3319
  }
2929
3320
 
3321
+ // src/commands/job.ts
3322
+ async function getClient() {
3323
+ const config = await getAuthConfig(process.env);
3324
+ return new ApiClient(config);
3325
+ }
3326
+ async function listJobs(options) {
3327
+ const client = await getClient();
3328
+ const response = await client.getJobStatus();
3329
+ if (options.json) {
3330
+ writeJson(response);
3331
+ return;
3332
+ }
3333
+ if (options.quiet) {
3334
+ const ids = response.data.jobs.map((job) => job.id).join("\n");
3335
+ if (ids) {
3336
+ writeStdout(ids);
3337
+ }
3338
+ return;
3339
+ }
3340
+ writeStdout(successLine(response.message));
3341
+ writeStdout();
3342
+ writeStdout(formatJobsList(response.data));
3343
+ }
3344
+ async function getJob(id, options) {
3345
+ const client = await getClient();
3346
+ const response = await client.getJobStatus();
3347
+ const job = response.data.jobs.find((j) => j.id === id);
3348
+ if (!job) {
3349
+ throw new CliError(`Job '${id}' not found.`, 1);
3350
+ }
3351
+ if (options.json) {
3352
+ writeJson({
3353
+ success: true,
3354
+ message: "Job loaded.",
3355
+ data: job,
3356
+ timestamp: response.timestamp
3357
+ });
3358
+ return;
3359
+ }
3360
+ if (options.quiet) {
3361
+ writeStdout(job.id);
3362
+ return;
3363
+ }
3364
+ writeStdout(successLine(`Job ${job.id} loaded.`));
3365
+ writeStdout();
3366
+ writeStdout(formatJobDetails(job));
3367
+ }
3368
+ async function runJob(id, options) {
3369
+ const client = await getClient();
3370
+ const response = await client.runJob(id);
3371
+ if (options.json) {
3372
+ writeJson(response);
3373
+ return;
3374
+ }
3375
+ if (options.quiet) {
3376
+ writeStdout(response.data.status);
3377
+ return;
3378
+ }
3379
+ writeStdout(successLine(response.message));
3380
+ writeStdout();
3381
+ writeStdout(formatRunJobResult(response.data));
3382
+ }
3383
+ async function runDueJobs(options) {
3384
+ const client = await getClient();
3385
+ const response = await client.runDueJobs();
3386
+ if (options.json) {
3387
+ writeJson(response);
3388
+ return;
3389
+ }
3390
+ if (options.quiet) {
3391
+ const statuses = response.data.results.map((r) => r.status).join("\n");
3392
+ if (statuses) {
3393
+ writeStdout(statuses);
3394
+ }
3395
+ return;
3396
+ }
3397
+ writeStdout(successLine(response.message));
3398
+ writeStdout();
3399
+ writeStdout(formatRunDueResult(response.data));
3400
+ }
3401
+ async function listJobHistory(id, options) {
3402
+ const client = await getClient();
3403
+ const response = await client.getJobStatus();
3404
+ const job = response.data.jobs.find((j) => j.id === id);
3405
+ if (!job) {
3406
+ throw new CliError(`Job '${id}' not found.`, 1);
3407
+ }
3408
+ if (options.json) {
3409
+ writeJson({
3410
+ success: true,
3411
+ message: "History loaded.",
3412
+ data: job.recent_runs || [],
3413
+ timestamp: response.timestamp
3414
+ });
3415
+ return;
3416
+ }
3417
+ if (options.quiet) {
3418
+ const ids = (job.recent_runs || []).map((r) => r.id).join("\n");
3419
+ if (ids) {
3420
+ writeStdout(ids);
3421
+ }
3422
+ return;
3423
+ }
3424
+ writeStdout(successLine(`History for job ${job.id} loaded.`));
3425
+ writeStdout();
3426
+ writeStdout(formatJobHistory(job.recent_runs || []));
3427
+ }
3428
+ async function clearJobHistory(options) {
3429
+ const client = await getClient();
3430
+ const response = await client.clearJobHistory(options.job);
3431
+ if (options.json) {
3432
+ writeJson(response);
3433
+ return;
3434
+ }
3435
+ if (options.quiet) {
3436
+ return;
3437
+ }
3438
+ writeStdout(successLine(response.message));
3439
+ }
3440
+ async function updateJobSchedule(id, options) {
3441
+ const client = await getClient();
3442
+ if (options.enabled && options.disabled) {
3443
+ throw new CliError("Cannot specify both --enabled and --disabled.", 1);
3444
+ }
3445
+ const payload = {};
3446
+ if (options.interval !== void 0) {
3447
+ const interval = parseInt(options.interval, 10);
3448
+ if (isNaN(interval) || interval <= 0) {
3449
+ throw new CliError("Interval must be a positive integer.", 1);
3450
+ }
3451
+ payload.interval_seconds = interval;
3452
+ }
3453
+ if (options.preferredTime !== void 0) {
3454
+ if (options.preferredTime.toLowerCase() === "none" || options.preferredTime === "") {
3455
+ payload.preferred_run_time = null;
3456
+ } else if (/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test(options.preferredTime)) {
3457
+ payload.preferred_run_time = options.preferredTime;
3458
+ } else {
3459
+ throw new CliError(
3460
+ "Preferred time must be in HH:MM format or 'none'.",
3461
+ 1
3462
+ );
3463
+ }
3464
+ }
3465
+ if (options.enabled !== void 0) {
3466
+ payload.is_enabled = true;
3467
+ } else if (options.disabled !== void 0) {
3468
+ payload.is_enabled = false;
3469
+ }
3470
+ if (Object.keys(payload).length === 0) {
3471
+ throw new CliError("No schedule changes requested.", 1);
3472
+ }
3473
+ const response = await client.updateJobSchedule(id, payload);
3474
+ if (options.json) {
3475
+ writeJson(response);
3476
+ return;
3477
+ }
3478
+ if (options.quiet) {
3479
+ return;
3480
+ }
3481
+ writeStdout(successLine(response.message));
3482
+ writeStdout();
3483
+ writeStdout(formatJobDetails(response.data));
3484
+ }
3485
+ async function resetJobSchedule(id, options) {
3486
+ const client = await getClient();
3487
+ const response = await client.resetJobSchedule(id);
3488
+ if (options.json) {
3489
+ writeJson(response);
3490
+ return;
3491
+ }
3492
+ if (options.quiet) {
3493
+ return;
3494
+ }
3495
+ writeStdout(successLine(response.message));
3496
+ writeStdout();
3497
+ writeStdout(formatJobDetails(response.data));
3498
+ }
3499
+ async function updateJobSettings(options) {
3500
+ const client = await getClient();
3501
+ if (options.retentionDays !== void 0) {
3502
+ const days = parseInt(options.retentionDays, 10);
3503
+ if (isNaN(days) || days < 0) {
3504
+ throw new CliError(
3505
+ "Retention days must be a non-negative integer.",
3506
+ 1
3507
+ );
3508
+ }
3509
+ const response2 = await client.updateJobSettings({
3510
+ retention_days: days
3511
+ });
3512
+ if (options.json) {
3513
+ writeJson(response2);
3514
+ return;
3515
+ }
3516
+ if (options.quiet) {
3517
+ writeStdout(String(response2.data.retention_days));
3518
+ return;
3519
+ }
3520
+ writeStdout(successLine(response2.message));
3521
+ return;
3522
+ }
3523
+ const response = await client.getJobStatus();
3524
+ if (options.json) {
3525
+ writeJson({
3526
+ success: true,
3527
+ message: "Settings loaded.",
3528
+ data: {
3529
+ retention_days: response.data.retention_days,
3530
+ timezone: response.data.timezone
3531
+ },
3532
+ timestamp: response.timestamp
3533
+ });
3534
+ return;
3535
+ }
3536
+ if (options.quiet) {
3537
+ writeStdout(String(response.data.retention_days));
3538
+ return;
3539
+ }
3540
+ writeStdout(successLine("Settings loaded."));
3541
+ writeStdout(`History retention: ${response.data.retention_days} days`);
3542
+ writeStdout(`Timezone: ${response.data.timezone}`);
3543
+ }
3544
+
2930
3545
  // src/index.ts
2931
3546
  function parseNumber(label) {
2932
3547
  return (value) => {
@@ -2947,12 +3562,12 @@ Examples:
2947
3562
  ${lines.map((line) => ` ${line}`).join("\n")}
2948
3563
 
2949
3564
  Documentation:
2950
- https://peakurl.org/docs/cli`
3565
+ https://go.peakurl.org/2aae02`
2951
3566
  );
2952
3567
  }
2953
3568
  async function getCliVersion() {
2954
3569
  const packageJson = new URL("../package.json", import.meta.url);
2955
- const content = await readFile3(packageJson, "utf8");
3570
+ const content = await readFile4(packageJson, "utf8");
2956
3571
  const parsed = JSON.parse(content);
2957
3572
  return parsed.version || "0.0.0";
2958
3573
  }
@@ -2961,7 +3576,7 @@ function getRetryCommandName(argv) {
2961
3576
  if (!first || first.startsWith("-")) {
2962
3577
  return void 0;
2963
3578
  }
2964
- if (first === "webhook" || first === "webhooks" || first === "activity" || first === "activities") {
3579
+ if (first === "webhook" || first === "activity" || first === "job") {
2965
3580
  const second = argv[3]?.trim();
2966
3581
  if (second && !second.startsWith("-")) {
2967
3582
  return `${first} ${second}`;
@@ -2969,10 +3584,33 @@ function getRetryCommandName(argv) {
2969
3584
  }
2970
3585
  return first;
2971
3586
  }
3587
+ var COMMAND_SUGGESTIONS = {
3588
+ activities: "activity",
3589
+ webhooks: "webhook",
3590
+ jobs: "job",
3591
+ cron: "job",
3592
+ "scheduled-jobs": "job",
3593
+ links: "list",
3594
+ urls: "list"
3595
+ };
2972
3596
  async function main() {
2973
3597
  const program = new Command();
2974
3598
  const version = await getCliVersion();
2975
- 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(
3599
+ 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({
3600
+ outputError: (str, write) => {
3601
+ const match = /error: unknown command '([^']+)'/.exec(str);
3602
+ if (match && COMMAND_SUGGESTIONS[match[1]]) {
3603
+ const suggestion = COMMAND_SUGGESTIONS[match[1]];
3604
+ write(
3605
+ `error: unknown command '${match[1]}'. Did you mean 'peakurl ${suggestion}'?
3606
+
3607
+ `
3608
+ );
3609
+ return;
3610
+ }
3611
+ write(str);
3612
+ }
3613
+ }).showHelpAfterError().showSuggestionAfterError().addHelpText(
2976
3614
  "after",
2977
3615
  `
2978
3616
  Get Started:
@@ -2984,13 +3622,16 @@ Common Commands:
2984
3622
  peakurl status
2985
3623
  peakurl core download
2986
3624
  peakurl list --limit 10
3625
+ peakurl edit docs --social-title "Docs" --social-image-url https://example.com/og.png
2987
3626
  peakurl import ./links.csv
2988
3627
  peakurl export --format csv
3628
+ peakurl activity list
3629
+ peakurl job list
2989
3630
  peakurl webhook list
2990
3631
  peakurl update --check
2991
3632
 
2992
3633
  Documentation:
2993
- https://peakurl.org/docs/cli
3634
+ https://go.peakurl.org/2aae02
2994
3635
 
2995
3636
  Run 'peakurl <command> --help' for command-specific flags and examples.`
2996
3637
  ).exitOverride();
@@ -3045,13 +3686,50 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
3045
3686
  ).option(
3046
3687
  "--expires-at <iso>",
3047
3688
  "Expiration timestamp in ISO-8601 format"
3048
- ).option("--utm-source <value>", "UTM source").option("--utm-medium <value>", "UTM medium").option("--utm-campaign <value>", "UTM campaign").option("--utm-term <value>", "UTM term").option("--utm-content <value>", "UTM content").option("--json", "Print machine-readable output").option("--quiet", "Print only the created short URL").action(createLink),
3689
+ ).option("--utm-source <value>", "UTM source").option("--utm-medium <value>", "UTM medium").option("--utm-campaign <value>", "UTM campaign").option("--utm-term <value>", "UTM term").option("--utm-content <value>", "UTM content").option("--social-title <title>", "Social preview title").option("--social-description <text>", "Social preview description").option(
3690
+ "--social-image-url <url>",
3691
+ "Remote image URL for social preview"
3692
+ ).option(
3693
+ "--social-image <path>",
3694
+ "Local image file path for social preview"
3695
+ ).option("--json", "Print machine-readable output").option("--quiet", "Print only the created short URL").action(createLink),
3049
3696
  [
3050
3697
  "peakurl create https://example.com/docs --alias docs",
3051
- 'peakurl create https://example.com/launch --title "Launch Page"',
3698
+ 'peakurl create https://example.com/launch --title "Launch Page" --social-title "Launch Page" --social-image-url https://example.com/og.png',
3052
3699
  "peakurl create https://example.com --json"
3053
3700
  ]
3054
3701
  );
3702
+ addExamples(
3703
+ program.command("edit").summary("Edit an existing short link").description(
3704
+ "Update an existing short link's destination URL, settings, or social preview by ID or alias."
3705
+ ).helpOption("-h, --help", "Show help").argument("<id-or-alias>", "Link identifier or alias").option("--url <url>", "Updated destination URL").option("--title <title>", "Updated title").option(
3706
+ "--password <password>",
3707
+ "Set or update password protection"
3708
+ ).option("--clear-password", "Remove password protection").option(
3709
+ "--status <status>",
3710
+ "Updated link status, for example active, inactive, or expired"
3711
+ ).option(
3712
+ "--expires-at <iso>",
3713
+ "Updated expiration timestamp in ISO-8601 format"
3714
+ ).option("--clear-expires-at", "Remove expiration timestamp").option("--social-title <title>", "Updated social preview title").option(
3715
+ "--social-description <text>",
3716
+ "Updated social preview description"
3717
+ ).option(
3718
+ "--social-image-url <url>",
3719
+ "Updated remote image URL for social preview"
3720
+ ).option(
3721
+ "--social-image <path>",
3722
+ "Upload a local image file for social preview"
3723
+ ).option(
3724
+ "--remove-social-image",
3725
+ "Remove existing social preview image"
3726
+ ).option("--json", "Print machine-readable output").option("--quiet", "Print only the short URL").action(editLink),
3727
+ [
3728
+ 'peakurl edit docs --social-title "PeakURL Docs" --social-description "Documentation and guides" --social-image-url https://peakurl.org/og.png',
3729
+ 'peakurl edit docs --url https://peakurl.org/docs/v2 --title "Updated Docs"',
3730
+ "peakurl edit url_123 --remove-social-image --json"
3731
+ ]
3732
+ );
3055
3733
  addExamples(
3056
3734
  program.command("import").summary("Import links from a file").description(
3057
3735
  "Import multiple short links from a local CSV, JSON, or XML file."
@@ -3104,7 +3782,7 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
3104
3782
  "peakurl delete --all"
3105
3783
  ]
3106
3784
  );
3107
- const activity = program.command("activity").alias("activities").summary("View and manage activity logs").helpOption("-h, --help", "Show help").description(
3785
+ const activity = program.command("activity").summary("View and manage activity logs").helpOption("-h, --help", "Show help").description(
3108
3786
  "View audit log activity entries, delete specific records, or clear all history."
3109
3787
  );
3110
3788
  addExamples(activity, [
@@ -3150,7 +3828,92 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
3150
3828
  ),
3151
3829
  ["peakurl update", "peakurl update --check", "peakurl update --json"]
3152
3830
  );
3153
- const webhook = program.command("webhook").alias("webhooks").summary("Manage webhooks").helpOption("-h, --help", "Show help").description("Manage outbound webhook integrations.");
3831
+ const jobCmd = program.command("job").summary("Manage scheduled jobs").description(
3832
+ "Manage server-side scheduled jobs, view their execution history, and run them manually."
3833
+ ).helpOption("-h, --help", "Show help");
3834
+ addExamples(jobCmd, [
3835
+ "peakurl job",
3836
+ "peakurl job list",
3837
+ "peakurl job get peakurl_version_check",
3838
+ "peakurl job run peakurl_version_check",
3839
+ "peakurl job run-due"
3840
+ ]);
3841
+ addExamples(
3842
+ 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),
3843
+ [
3844
+ "peakurl job",
3845
+ "peakurl job list",
3846
+ "peakurl job list --json",
3847
+ "peakurl job list --quiet"
3848
+ ]
3849
+ );
3850
+ addExamples(
3851
+ jobCmd.command("get").summary("Show job details").description(
3852
+ "Show detailed configuration and status for one scheduled job."
3853
+ ).helpOption("-h, --help", "Show help").argument("<id>", "Job identifier").option("--json", "Print machine-readable output").option("--quiet", "Print only the job ID").action(getJob),
3854
+ [
3855
+ "peakurl job get peakurl_version_check",
3856
+ "peakurl job get peakurl_version_check --json"
3857
+ ]
3858
+ );
3859
+ addExamples(
3860
+ 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),
3861
+ [
3862
+ "peakurl job run peakurl_version_check",
3863
+ "peakurl job run peakurl_version_check --json"
3864
+ ]
3865
+ );
3866
+ addExamples(
3867
+ 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),
3868
+ ["peakurl job run-due", "peakurl job run-due --json"]
3869
+ );
3870
+ addExamples(
3871
+ 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),
3872
+ [
3873
+ "peakurl job history peakurl_version_check",
3874
+ "peakurl job history peakurl_version_check --json"
3875
+ ]
3876
+ );
3877
+ addExamples(
3878
+ jobCmd.command("clear-history").summary("Clear job history").description(
3879
+ "Clear execution history for all jobs or a specific job."
3880
+ ).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),
3881
+ [
3882
+ "peakurl job clear-history",
3883
+ "peakurl job clear-history --job peakurl_version_check",
3884
+ "peakurl job clear-history --json"
3885
+ ]
3886
+ );
3887
+ addExamples(
3888
+ 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(
3889
+ "--preferred-time <time>",
3890
+ "Preferred run time (HH:MM or 'none')"
3891
+ ).option("--enabled", "Enable the job").option("--disabled", "Disable the job").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(updateJobSchedule),
3892
+ [
3893
+ "peakurl job schedule peakurl_version_check --interval 43200",
3894
+ "peakurl job schedule peakurl_version_check --preferred-time 03:00",
3895
+ "peakurl job schedule peakurl_version_check --disabled"
3896
+ ]
3897
+ );
3898
+ addExamples(
3899
+ 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),
3900
+ [
3901
+ "peakurl job reset peakurl_version_check",
3902
+ "peakurl job reset peakurl_version_check --json"
3903
+ ]
3904
+ );
3905
+ addExamples(
3906
+ jobCmd.command("settings").summary("Manage scheduler settings").description("View or update global scheduler settings.").helpOption("-h, --help", "Show help").option(
3907
+ "--retention-days <days>",
3908
+ "Number of days to keep execution history"
3909
+ ).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action(updateJobSettings),
3910
+ [
3911
+ "peakurl job settings",
3912
+ "peakurl job settings --retention-days 14",
3913
+ "peakurl job settings --json"
3914
+ ]
3915
+ );
3916
+ const webhook = program.command("webhook").summary("Manage webhooks").helpOption("-h, --help", "Show help").description("Manage outbound webhook integrations.");
3154
3917
  addExamples(webhook, [
3155
3918
  "peakurl webhook",
3156
3919
  "peakurl webhook list",
package/man/peakurl.1 CHANGED
@@ -25,6 +25,24 @@
25
25
  .Ar url
26
26
  .Op Fl -alias Ar alias
27
27
  .Op Fl -title Ar title
28
+ .Op Fl -social-title Ar title
29
+ .Op Fl -social-description Ar text
30
+ .Op Fl -social-image-url Ar url
31
+ .Op Fl -social-image Ar path
32
+ .Op Fl -json
33
+ .Op Fl -quiet
34
+ .Pp
35
+ .Nm
36
+ .Cm edit
37
+ .Ar id-or-alias
38
+ .Op Fl -url Ar url
39
+ .Op Fl -alias Ar alias
40
+ .Op Fl -title Ar title
41
+ .Op Fl -social-title Ar title
42
+ .Op Fl -social-description Ar text
43
+ .Op Fl -social-image-url Ar url
44
+ .Op Fl -social-image Ar path
45
+ .Op Fl -remove-social-image
28
46
  .Op Fl -json
29
47
  .Op Fl -quiet
30
48
  .Pp
@@ -44,8 +62,8 @@
44
62
  .Sh DESCRIPTION
45
63
  .Nm
46
64
  is the official command-line interface for PeakURL.
47
- It wraps the PeakURL HTTP API for creating, listing, importing, exporting,
48
- and deleting short links.
65
+ It wraps the PeakURL HTTP API for creating, editing, listing, importing,
66
+ exporting, and deleting short links.
49
67
  It also supports authenticated account inspection, site system status,
50
68
  outbound webhooks, CLI update checks, and public PeakURL core package
51
69
  downloads.
@@ -136,7 +154,15 @@ peakurl core download --force
136
154
  Create a short link for a destination URL.
137
155
  .Bd -literal -offset indent
138
156
  peakurl create https://example.com/docs --alias docs
139
- peakurl create https://example.com/launch --title "Launch Page"
157
+ peakurl create https://example.com/launch --title "Launch Page" --social-title "Launch Page" --social-image-url https://example.com/og.png
158
+ .Ed
159
+ .It Cm edit Ar id-or-alias
160
+ Update an existing short link's destination URL, title, status, expiration,
161
+ password, or social preview fields by ID, alias, or short code.
162
+ .Bd -literal -offset indent
163
+ peakurl edit docs --social-title "PeakURL Docs" --social-description "Documentation and guides" --social-image-url https://peakurl.org/og.png
164
+ peakurl edit docs --url https://peakurl.org/docs/v2 --title "Updated Docs"
165
+ peakurl edit url_123 --remove-social-image
140
166
  .Ed
141
167
  .It Cm import Ar file
142
168
  Import short links from a local CSV, JSON, or XML file.
@@ -195,6 +221,54 @@ List supported webhook event identifiers.
195
221
  .Bd -literal -offset indent
196
222
  peakurl webhook events
197
223
  .Ed
224
+ .It Cm job list
225
+ List all registered scheduled jobs.
226
+ .Bd -literal -offset indent
227
+ peakurl job list
228
+ peakurl job list --json
229
+ .Ed
230
+ .It Cm job get Ar id
231
+ Fetch one scheduled job by ID.
232
+ .Bd -literal -offset indent
233
+ peakurl job get peakurl_version_check
234
+ .Ed
235
+ .It Cm job run Ar id
236
+ Force a specific scheduled job to run immediately.
237
+ .Bd -literal -offset indent
238
+ peakurl job run peakurl_version_check
239
+ .Ed
240
+ .It Cm job run-due
241
+ Trigger all scheduled jobs that are currently due.
242
+ .Bd -literal -offset indent
243
+ peakurl job run-due
244
+ .Ed
245
+ .It Cm job history Ar id
246
+ View recent execution history for a scheduled job.
247
+ .Bd -literal -offset indent
248
+ peakurl job history peakurl_version_check
249
+ .Ed
250
+ .It Cm job clear-history
251
+ Clear execution history for all jobs or a specific job.
252
+ .Bd -literal -offset indent
253
+ peakurl job clear-history
254
+ peakurl job clear-history --job peakurl_version_check
255
+ .Ed
256
+ .It Cm job schedule Ar id
257
+ Update the schedule configuration for a job.
258
+ .Bd -literal -offset indent
259
+ peakurl job schedule peakurl_version_check --interval 43200 --preferred-time 03:00 --enabled
260
+ .Ed
261
+ .It Cm job reset Ar id
262
+ Reset a job's schedule to its default configuration.
263
+ .Bd -literal -offset indent
264
+ peakurl job reset peakurl_version_check
265
+ .Ed
266
+ .It Cm job settings
267
+ View or update global scheduler settings.
268
+ .Bd -literal -offset indent
269
+ peakurl job settings
270
+ peakurl job settings --retention-days 14
271
+ .Ed
198
272
  .It Cm update
199
273
  Check for a newer CLI version and print the npm install command when an
200
274
  update is available.
@@ -220,14 +294,18 @@ Print machine-readable JSON where supported.
220
294
  Print minimal output for scripts or suppress success output for destructive
221
295
  commands.
222
296
  .El
223
- .Ss Link Creation Options
224
- .Bl -tag -width "--utm-campaign"
297
+ .Ss Link Creation And Editing Options
298
+ .Bl -tag -width "--remove-social-image"
299
+ .It Fl -url Ar url
300
+ Updated destination URL when editing an existing short link.
225
301
  .It Fl -alias Ar alias
226
- Custom alias for the short link.
302
+ Custom alias when creating a short link.
227
303
  .It Fl -title Ar title
228
304
  Title stored with the short link.
229
305
  .It Fl -password Ar password
230
306
  Password-protect the short link.
307
+ .It Fl -clear-password
308
+ Remove password protection when editing a short link.
231
309
  .It Fl -status Ar status
232
310
  Link status, for example
233
311
  .Sy active
@@ -235,16 +313,28 @@ or
235
313
  .Sy paused .
236
314
  .It Fl -expires-at Ar iso
237
315
  Expiration timestamp in ISO-8601 format.
316
+ .It Fl -clear-expires-at
317
+ Remove the expiration timestamp when editing a short link.
238
318
  .It Fl -utm-source Ar value
239
- UTM source value.
319
+ UTM source value when creating a short link.
240
320
  .It Fl -utm-medium Ar value
241
- UTM medium value.
321
+ UTM medium value when creating a short link.
242
322
  .It Fl -utm-campaign Ar value
243
- UTM campaign value.
323
+ UTM campaign value when creating a short link.
244
324
  .It Fl -utm-term Ar value
245
- UTM term value.
325
+ UTM term value when creating a short link.
246
326
  .It Fl -utm-content Ar value
247
- UTM content value.
327
+ UTM content value when creating a short link.
328
+ .It Fl -social-title Ar title
329
+ Social preview (Open Graph / Twitter Card) title.
330
+ .It Fl -social-description Ar text
331
+ Social preview description.
332
+ .It Fl -social-image-url Ar url
333
+ Remote image URL for the social preview card.
334
+ .It Fl -social-image Ar path
335
+ Local image file (JPG, PNG, WEBP, or GIF up to 5 MB) to upload for the social preview card.
336
+ .It Fl -remove-social-image
337
+ Remove the existing social preview image when editing a short link.
248
338
  .El
249
339
  .Ss List Options
250
340
  .Bl -tag -width "--sort-order"
@@ -346,6 +436,7 @@ when environment credentials should no longer apply.
346
436
  .Bd -literal -offset indent
347
437
  peakurl login --base-url https://example.com/api/v1 --api-key YOUR_API_KEY
348
438
  peakurl create https://example.com/docs --alias docs
439
+ peakurl edit docs --social-title "PeakURL Docs" --social-image-url https://peakurl.org/og.png
349
440
  peakurl list --limit 10
350
441
  peakurl get docs
351
442
  peakurl export --format json --stdout
@@ -353,11 +444,11 @@ peakurl webhook create https://example.com/api/webhooks/peakurl --event link.cli
353
444
  peakurl core download
354
445
  .Ed
355
446
  .Sh SEE ALSO
356
- .Lk https://peakurl.org/docs/cli "PeakURL CLI documentation"
447
+ .Lk https://go.peakurl.org/2aae02 "PeakURL CLI documentation"
357
448
  .Pp
358
- .Lk https://peakurl.org/docs/api "PeakURL API documentation"
449
+ .Lk https://go.peakurl.org/d373f6 "PeakURL API documentation"
359
450
  .Pp
360
- .Lk https://www.npmjs.com/package/peakurl "peakurl npm package"
451
+ .Lk https://go.peakurl.org/cli "peakurl npm package"
361
452
  .Sh BUGS
362
453
  Report issues at
363
454
  .Lk https://github.com/PeakURL/CLI/issues .
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peakurl",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Official CLI for creating, listing, and managing PeakURL short links from the terminal",
5
5
  "homepage": "https://peakurl.org",
6
6
  "bugs": {
@@ -55,16 +55,16 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@eslint/js": "^10.0.1",
58
- "@types/node": "^26.4.1",
59
- "eslint": "^10.9.1",
58
+ "@types/node": "^26.6.2",
59
+ "eslint": "^10.11.0",
60
60
  "eslint-config-prettier": "^10.1.8",
61
61
  "eslint-plugin-prettier": "^5.5.6",
62
62
  "globals": "^17.12.0",
63
- "prettier": "^3.9.6",
63
+ "prettier": "^3.9.8",
64
64
  "tsup": "^8.5.1",
65
- "tsx": "^4.23.13",
65
+ "tsx": "^4.23.15",
66
66
  "typescript": "^5.9.3",
67
- "typescript-eslint": "^8.69.0"
67
+ "typescript-eslint": "^8.70.1"
68
68
  },
69
69
  "license": "MIT"
70
70
  }