peakurl 1.1.1 → 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://go.peakurl.org/7a0e0b>
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. |
@@ -84,9 +85,24 @@ Create a short link:
84
85
  peakurl create \
85
86
  https://example.com \
86
87
  --alias example \
87
- --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
88
92
  ```
89
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
+
90
106
  List links as JSON:
91
107
 
92
108
  ```bash
@@ -268,8 +284,8 @@ export PEAKURL_DISABLE_UPDATE_CHECK=1
268
284
 
269
285
  ## Links
270
286
 
271
- - Website: <https://peakurl.org/>
272
- - CLI docs: <https://go.peakurl.org/7a0e0b>
273
- - API docs: <https://go.peakurl.org/d373f6>
274
- - npm package: <https://www.npmjs.com/package/peakurl>
275
- - 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
@@ -145,8 +197,38 @@ var ApiClient = class {
145
197
  * @param payload Request body accepted by `POST /api/v1/urls`.
146
198
  * @returns API response envelope containing the created link.
147
199
  */
148
- createUrl(payload) {
149
- 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);
150
232
  }
151
233
  /**
152
234
  * Lists short URLs with optional pagination and filtering.
@@ -406,15 +488,16 @@ var ApiClient = class {
406
488
  async request(method, path, body, query) {
407
489
  const url = buildApiUrl(this.config.apiBaseUrl, path, query);
408
490
  let response;
491
+ const isFormData = body instanceof FormData;
409
492
  try {
410
493
  response = await fetch(url, {
411
494
  method,
412
495
  headers: {
413
496
  Accept: "application/json",
414
497
  Authorization: `Bearer ${this.config.apiKey}`,
415
- ...body ? { "Content-Type": "application/json" } : {}
498
+ ...body && !isFormData ? { "Content-Type": "application/json" } : {}
416
499
  },
417
- body: body ? JSON.stringify(body) : void 0
500
+ body: isFormData ? body : body ? JSON.stringify(body) : void 0
418
501
  });
419
502
  } catch (error) {
420
503
  throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
@@ -692,7 +775,7 @@ function formatActivitySummary(data, count) {
692
775
  }
693
776
 
694
777
  // src/config/store.ts
695
- import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
778
+ import { chmod, mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
696
779
  import { dirname, join } from "path";
697
780
  import envPaths from "env-paths";
698
781
  var CONFIG_FILENAME = "config.json";
@@ -731,7 +814,7 @@ var ConfigStore = class {
731
814
  */
732
815
  async load() {
733
816
  try {
734
- const content = await readFile(this.filePath, "utf8");
817
+ const content = await readFile2(this.filePath, "utf8");
735
818
  const parsed = JSON.parse(content);
736
819
  const apiBaseUrl = typeof parsed?.apiBaseUrl === "string" ? parsed.apiBaseUrl : typeof parsed?.baseUrl === "string" ? parsed.baseUrl : void 0;
737
820
  if (typeof apiBaseUrl !== "string" || typeof parsed?.apiKey !== "string") {
@@ -821,7 +904,7 @@ var StateStore = class {
821
904
  */
822
905
  async load() {
823
906
  try {
824
- const content = await readFile(this.filePath, "utf8");
907
+ const content = await readFile2(this.filePath, "utf8");
825
908
  const parsed = JSON.parse(content);
826
909
  return parsed && typeof parsed === "object" ? parsed : {};
827
910
  } catch {
@@ -1240,7 +1323,10 @@ var EXPORT_HEADERS = [
1240
1323
  "short_url",
1241
1324
  "clicks",
1242
1325
  "unique_clicks",
1243
- "created_at"
1326
+ "created_at",
1327
+ "social_title",
1328
+ "social_description",
1329
+ "social_image_url"
1244
1330
  ];
1245
1331
  function text(value) {
1246
1332
  if (typeof value === "string") {
@@ -1299,7 +1385,10 @@ function buildExportRows(links) {
1299
1385
  short_url: text(link.shortUrl),
1300
1386
  clicks: typeof link.clicks === "number" ? link.clicks : "",
1301
1387
  unique_clicks: typeof link.uniqueClicks === "number" ? link.uniqueClicks : "",
1302
- 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)
1303
1392
  }));
1304
1393
  }
1305
1394
  function serializeLinkExport(links, format) {
@@ -1319,6 +1408,9 @@ function serializeLinkExport(links, format) {
1319
1408
  <clicks>${xmlValue(row2.clicks)}</clicks>
1320
1409
  <uniqueClicks>${xmlValue(row2.unique_clicks)}</uniqueClicks>
1321
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>
1322
1414
  </url>`
1323
1415
  ).join("\n");
1324
1416
  return `<urls>
@@ -1337,7 +1429,7 @@ ${body}
1337
1429
  }
1338
1430
 
1339
1431
  // src/lib/imports.ts
1340
- import { readFile as readFile2 } from "fs/promises";
1432
+ import { readFile as readFile3 } from "fs/promises";
1341
1433
  function text2(value) {
1342
1434
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1343
1435
  }
@@ -1424,7 +1516,16 @@ function normalizeImportRow(value) {
1424
1516
  ...text2(value.utmMedium) ? { utmMedium: text2(value.utmMedium) } : {},
1425
1517
  ...text2(value.utmCampaign) ? { utmCampaign: text2(value.utmCampaign) } : {},
1426
1518
  ...text2(value.utmTerm) ? { utmTerm: text2(value.utmTerm) } : {},
1427
- ...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
+ } : {}
1428
1529
  };
1429
1530
  }
1430
1531
  function parseJson(text6) {
@@ -1495,6 +1596,18 @@ function parseCsv(text6) {
1495
1596
  }
1496
1597
  if (header === "utmcontent") {
1497
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;
1498
1611
  }
1499
1612
  });
1500
1613
  const link = normalizeImportRow(entry);
@@ -1532,7 +1645,10 @@ function parseXml(text6) {
1532
1645
  utmMedium: getValue("utmMedium"),
1533
1646
  utmCampaign: getValue("utmCampaign"),
1534
1647
  utmTerm: getValue("utmTerm"),
1535
- utmContent: getValue("utmContent")
1648
+ utmContent: getValue("utmContent"),
1649
+ socialTitle: getValue("socialTitle"),
1650
+ socialDescription: getValue("socialDescription"),
1651
+ socialImageUrl: getValue("socialImageUrl")
1536
1652
  });
1537
1653
  }).filter((item) => Boolean(item));
1538
1654
  }
@@ -1551,7 +1667,7 @@ function getImportFormat(filePath, value) {
1551
1667
  async function readImportRows(filePath, format) {
1552
1668
  let textContent;
1553
1669
  try {
1554
- textContent = await readFile2(filePath, "utf8");
1670
+ textContent = await readFile3(filePath, "utf8");
1555
1671
  } catch (error) {
1556
1672
  throw new CliError(`Could not read import file ${filePath}.`, 1, {
1557
1673
  cause: error instanceof Error ? error : void 0
@@ -1672,6 +1788,10 @@ function getQuietLinkValue(link) {
1672
1788
  return getLinkShortUrl(link) || getLinkAlias(link) || getLinkId(link) || "";
1673
1789
  }
1674
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);
1675
1795
  const rows2 = [
1676
1796
  ["ID", getLinkId(link)],
1677
1797
  ["Alias", getLinkAlias(link)],
@@ -1679,6 +1799,9 @@ function formatLinkDetails(link) {
1679
1799
  ["Destination", getLinkDestination(link)],
1680
1800
  ["Title", asString3(link.title)],
1681
1801
  ["Status", asString3(link.status)],
1802
+ ["Social Title", socialTitle],
1803
+ ["Social Description", socialDescription],
1804
+ ["Social Image", socialImage],
1682
1805
  [
1683
1806
  "Clicks",
1684
1807
  asNumber2(link.clicks) === void 0 ? void 0 : String(link.clicks)
@@ -2667,6 +2790,11 @@ function normalizeExpiresAt(value) {
2667
2790
  return value;
2668
2791
  }
2669
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
+ }
2670
2798
  const config = await getAuthConfig(process.env);
2671
2799
  const response = await new ApiClient(config).createUrl({
2672
2800
  destinationUrl: normalizeDestinationUrl(destinationUrl),
@@ -2679,7 +2807,15 @@ async function createLink(destinationUrl, options) {
2679
2807
  ...options.utmMedium ? { utmMedium: options.utmMedium } : {},
2680
2808
  ...options.utmCampaign ? { utmCampaign: options.utmCampaign } : {},
2681
2809
  ...options.utmTerm ? { utmTerm: options.utmTerm } : {},
2682
- ...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) } : {}
2683
2819
  });
2684
2820
  if (options.json) {
2685
2821
  writeJson(response);
@@ -2692,6 +2828,67 @@ async function createLink(destinationUrl, options) {
2692
2828
  writeStdout(successLine(response.message));
2693
2829
  writeStdout(formatLinkDetails(response.data));
2694
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
+ }
2695
2892
  async function importLinks(filePath, options) {
2696
2893
  const format = getImportFormat(filePath, options.format);
2697
2894
  const urls = await readImportRows(filePath, format);
@@ -3365,12 +3562,12 @@ Examples:
3365
3562
  ${lines.map((line) => ` ${line}`).join("\n")}
3366
3563
 
3367
3564
  Documentation:
3368
- https://peakurl.org/docs/cli`
3565
+ https://go.peakurl.org/2aae02`
3369
3566
  );
3370
3567
  }
3371
3568
  async function getCliVersion() {
3372
3569
  const packageJson = new URL("../package.json", import.meta.url);
3373
- const content = await readFile3(packageJson, "utf8");
3570
+ const content = await readFile4(packageJson, "utf8");
3374
3571
  const parsed = JSON.parse(content);
3375
3572
  return parsed.version || "0.0.0";
3376
3573
  }
@@ -3425,6 +3622,7 @@ Common Commands:
3425
3622
  peakurl status
3426
3623
  peakurl core download
3427
3624
  peakurl list --limit 10
3625
+ peakurl edit docs --social-title "Docs" --social-image-url https://example.com/og.png
3428
3626
  peakurl import ./links.csv
3429
3627
  peakurl export --format csv
3430
3628
  peakurl activity list
@@ -3433,7 +3631,7 @@ Common Commands:
3433
3631
  peakurl update --check
3434
3632
 
3435
3633
  Documentation:
3436
- https://peakurl.org/docs/cli
3634
+ https://go.peakurl.org/2aae02
3437
3635
 
3438
3636
  Run 'peakurl <command> --help' for command-specific flags and examples.`
3439
3637
  ).exitOverride();
@@ -3488,13 +3686,50 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
3488
3686
  ).option(
3489
3687
  "--expires-at <iso>",
3490
3688
  "Expiration timestamp in ISO-8601 format"
3491
- ).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),
3492
3696
  [
3493
3697
  "peakurl create https://example.com/docs --alias docs",
3494
- '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',
3495
3699
  "peakurl create https://example.com --json"
3496
3700
  ]
3497
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
+ );
3498
3733
  addExamples(
3499
3734
  program.command("import").summary("Import links from a file").description(
3500
3735
  "Import multiple short links from a local CSV, JSON, or XML file."
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.
@@ -268,14 +294,18 @@ Print machine-readable JSON where supported.
268
294
  Print minimal output for scripts or suppress success output for destructive
269
295
  commands.
270
296
  .El
271
- .Ss Link Creation Options
272
- .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.
273
301
  .It Fl -alias Ar alias
274
- Custom alias for the short link.
302
+ Custom alias when creating a short link.
275
303
  .It Fl -title Ar title
276
304
  Title stored with the short link.
277
305
  .It Fl -password Ar password
278
306
  Password-protect the short link.
307
+ .It Fl -clear-password
308
+ Remove password protection when editing a short link.
279
309
  .It Fl -status Ar status
280
310
  Link status, for example
281
311
  .Sy active
@@ -283,16 +313,28 @@ or
283
313
  .Sy paused .
284
314
  .It Fl -expires-at Ar iso
285
315
  Expiration timestamp in ISO-8601 format.
316
+ .It Fl -clear-expires-at
317
+ Remove the expiration timestamp when editing a short link.
286
318
  .It Fl -utm-source Ar value
287
- UTM source value.
319
+ UTM source value when creating a short link.
288
320
  .It Fl -utm-medium Ar value
289
- UTM medium value.
321
+ UTM medium value when creating a short link.
290
322
  .It Fl -utm-campaign Ar value
291
- UTM campaign value.
323
+ UTM campaign value when creating a short link.
292
324
  .It Fl -utm-term Ar value
293
- UTM term value.
325
+ UTM term value when creating a short link.
294
326
  .It Fl -utm-content Ar value
295
- 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.
296
338
  .El
297
339
  .Ss List Options
298
340
  .Bl -tag -width "--sort-order"
@@ -394,6 +436,7 @@ when environment credentials should no longer apply.
394
436
  .Bd -literal -offset indent
395
437
  peakurl login --base-url https://example.com/api/v1 --api-key YOUR_API_KEY
396
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
397
440
  peakurl list --limit 10
398
441
  peakurl get docs
399
442
  peakurl export --format json --stdout
@@ -401,11 +444,11 @@ peakurl webhook create https://example.com/api/webhooks/peakurl --event link.cli
401
444
  peakurl core download
402
445
  .Ed
403
446
  .Sh SEE ALSO
404
- .Lk https://peakurl.org/docs/cli "PeakURL CLI documentation"
447
+ .Lk https://go.peakurl.org/2aae02 "PeakURL CLI documentation"
405
448
  .Pp
406
- .Lk https://peakurl.org/docs/api "PeakURL API documentation"
449
+ .Lk https://go.peakurl.org/d373f6 "PeakURL API documentation"
407
450
  .Pp
408
- .Lk https://www.npmjs.com/package/peakurl "peakurl npm package"
451
+ .Lk https://go.peakurl.org/cli "peakurl npm package"
409
452
  .Sh BUGS
410
453
  Report issues at
411
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.1",
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": {