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 +23 -7
- package/bin/peakurl.js +255 -20
- package/man/peakurl.1 +57 -14
- package/package.json +1 -1
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
|
|
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
|
|
272
|
-
- CLI docs
|
|
273
|
-
- API docs
|
|
274
|
-
- npm package
|
|
275
|
-
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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/
|
|
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("--
|
|
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,
|
|
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 "--
|
|
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
|
|
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/
|
|
447
|
+
.Lk https://go.peakurl.org/2aae02 "PeakURL CLI documentation"
|
|
405
448
|
.Pp
|
|
406
|
-
.Lk https://peakurl.org/
|
|
449
|
+
.Lk https://go.peakurl.org/d373f6 "PeakURL API documentation"
|
|
407
450
|
.Pp
|
|
408
|
-
.Lk https://
|
|
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 .
|