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