peakurl 1.0.3 → 1.1.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 +55 -25
- package/bin/peakurl.js +976 -543
- package/package.json +13 -4
package/bin/peakurl.js
CHANGED
|
@@ -4,14 +4,6 @@
|
|
|
4
4
|
import { readFile as readFile3 } from "fs/promises";
|
|
5
5
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
6
6
|
|
|
7
|
-
// src/commands/core.ts
|
|
8
|
-
import { cwd } from "process";
|
|
9
|
-
|
|
10
|
-
// src/config/store.ts
|
|
11
|
-
import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
12
|
-
import { dirname, join } from "path";
|
|
13
|
-
import envPaths from "env-paths";
|
|
14
|
-
|
|
15
7
|
// src/lib/errors.ts
|
|
16
8
|
var CliError = class extends Error {
|
|
17
9
|
exitCode;
|
|
@@ -33,159 +25,6 @@ function ensureCliError(error) {
|
|
|
33
25
|
return new CliError("Unexpected error.");
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
// src/config/store.ts
|
|
37
|
-
var CONFIG_FILENAME = "config.json";
|
|
38
|
-
var STATE_FILENAME = "state.json";
|
|
39
|
-
function getConfigPath() {
|
|
40
|
-
const paths = envPaths("peakurl", { suffix: "" });
|
|
41
|
-
const directory = process.platform === "darwin" ? paths.data : paths.config;
|
|
42
|
-
return join(directory, CONFIG_FILENAME);
|
|
43
|
-
}
|
|
44
|
-
function getStatePath() {
|
|
45
|
-
return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
|
|
46
|
-
}
|
|
47
|
-
async function ensureParentDir(filePath) {
|
|
48
|
-
const directory = dirname(filePath);
|
|
49
|
-
await mkdir(directory, { recursive: true, mode: 448 });
|
|
50
|
-
return directory;
|
|
51
|
-
}
|
|
52
|
-
var ConfigStore = class {
|
|
53
|
-
filePath;
|
|
54
|
-
/**
|
|
55
|
-
* Creates a config store bound to one on-disk file.
|
|
56
|
-
*
|
|
57
|
-
* @param filePath Optional override used by tests or advanced callers.
|
|
58
|
-
*/
|
|
59
|
-
constructor(filePath = getConfigPath()) {
|
|
60
|
-
this.filePath = filePath;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Loads the stored credential set from disk.
|
|
64
|
-
*
|
|
65
|
-
* Missing files are treated as "not configured yet" instead of as hard
|
|
66
|
-
* errors so first-run CLI flows remain clean.
|
|
67
|
-
*
|
|
68
|
-
* @returns Stored config or `null` when the file does not exist.
|
|
69
|
-
* @throws {CliError} When the file exists but is unreadable or invalid.
|
|
70
|
-
*/
|
|
71
|
-
async load() {
|
|
72
|
-
try {
|
|
73
|
-
const content = await readFile(this.filePath, "utf8");
|
|
74
|
-
const parsed = JSON.parse(content);
|
|
75
|
-
const apiBaseUrl = typeof parsed?.apiBaseUrl === "string" ? parsed.apiBaseUrl : typeof parsed?.baseUrl === "string" ? parsed.baseUrl : void 0;
|
|
76
|
-
if (typeof apiBaseUrl !== "string" || typeof parsed?.apiKey !== "string") {
|
|
77
|
-
throw new CliError(`Invalid config file: ${this.filePath}`);
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
apiBaseUrl,
|
|
81
|
-
apiKey: parsed.apiKey
|
|
82
|
-
};
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code === "ENOENT") {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
if (error instanceof CliError) {
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
throw new CliError(
|
|
91
|
-
`Could not read PeakURL config at ${this.filePath}.`,
|
|
92
|
-
1,
|
|
93
|
-
{
|
|
94
|
-
cause: error instanceof Error ? error : void 0
|
|
95
|
-
}
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Persists one credential set to disk with restrictive file permissions.
|
|
101
|
-
*
|
|
102
|
-
* The chmod step is best-effort because Windows and some filesystems do not
|
|
103
|
-
* expose POSIX permission bits in the same way as Unix-like systems.
|
|
104
|
-
*
|
|
105
|
-
* @param config Normalized credential set to write.
|
|
106
|
-
*/
|
|
107
|
-
async save(config) {
|
|
108
|
-
const directory = await ensureParentDir(this.filePath);
|
|
109
|
-
await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
|
|
110
|
-
`, {
|
|
111
|
-
mode: 384
|
|
112
|
-
});
|
|
113
|
-
try {
|
|
114
|
-
await chmod(directory, 448);
|
|
115
|
-
await chmod(this.filePath, 384);
|
|
116
|
-
} catch {
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Removes the stored credential file.
|
|
121
|
-
*
|
|
122
|
-
* @returns `true` when a saved config file existed and was removed.
|
|
123
|
-
* @throws {CliError} When the file exists but cannot be removed.
|
|
124
|
-
*/
|
|
125
|
-
async clear() {
|
|
126
|
-
try {
|
|
127
|
-
await unlink(this.filePath);
|
|
128
|
-
return true;
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code === "ENOENT") {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
throw new CliError(
|
|
134
|
-
`Could not remove PeakURL config at ${this.filePath}.`,
|
|
135
|
-
1,
|
|
136
|
-
{
|
|
137
|
-
cause: error instanceof Error ? error : void 0
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
var StateStore = class {
|
|
144
|
-
filePath;
|
|
145
|
-
/**
|
|
146
|
-
* Creates a state store bound to one on-disk file.
|
|
147
|
-
*
|
|
148
|
-
* @param filePath Optional override used by tests or advanced callers.
|
|
149
|
-
*/
|
|
150
|
-
constructor(filePath = getStatePath()) {
|
|
151
|
-
this.filePath = filePath;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Loads cached state from disk.
|
|
155
|
-
*
|
|
156
|
-
* Missing or invalid files are treated as empty state because the CLI can
|
|
157
|
-
* always recompute update metadata on the next successful network check.
|
|
158
|
-
*
|
|
159
|
-
* @returns Parsed state object or an empty object.
|
|
160
|
-
*/
|
|
161
|
-
async load() {
|
|
162
|
-
try {
|
|
163
|
-
const content = await readFile(this.filePath, "utf8");
|
|
164
|
-
const parsed = JSON.parse(content);
|
|
165
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
166
|
-
} catch {
|
|
167
|
-
return {};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Persists cached state to disk.
|
|
172
|
-
*
|
|
173
|
-
* @param state State payload to save.
|
|
174
|
-
*/
|
|
175
|
-
async save(state) {
|
|
176
|
-
const directory = await ensureParentDir(this.filePath);
|
|
177
|
-
await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
|
|
178
|
-
`, {
|
|
179
|
-
mode: 384
|
|
180
|
-
});
|
|
181
|
-
try {
|
|
182
|
-
await chmod(directory, 448);
|
|
183
|
-
await chmod(this.filePath, 384);
|
|
184
|
-
} catch {
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
28
|
// src/lib/url.ts
|
|
190
29
|
function validateHttpUrl(parsed, label) {
|
|
191
30
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
@@ -260,72 +99,289 @@ function normalizeWebhookUrl(value) {
|
|
|
260
99
|
}
|
|
261
100
|
}
|
|
262
101
|
|
|
263
|
-
// src/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const retryCommand = commandName ? `peakurl ${commandName}` : "peakurl whoami";
|
|
269
|
-
const rows2 = [
|
|
270
|
-
[
|
|
271
|
-
"Save credentials",
|
|
272
|
-
`peakurl login --base-url ${EXAMPLE_BASE_URL}
|
|
273
|
-
--api-key ${EXAMPLE_API_KEY}`,
|
|
274
|
-
"Regular use on this machine"
|
|
275
|
-
],
|
|
276
|
-
[
|
|
277
|
-
"Set environment variables",
|
|
278
|
-
`PEAKURL_BASE_URL=${EXAMPLE_BASE_URL}
|
|
279
|
-
PEAKURL_API_KEY=${EXAMPLE_API_KEY}`,
|
|
280
|
-
"CI, scripts, or one-off use"
|
|
281
|
-
],
|
|
282
|
-
["Then run", retryCommand, "After completing one of the steps above"]
|
|
283
|
-
];
|
|
284
|
-
return rows2;
|
|
102
|
+
// src/api/client.ts
|
|
103
|
+
function isApiResponse(value) {
|
|
104
|
+
return Boolean(
|
|
105
|
+
value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
|
|
106
|
+
);
|
|
285
107
|
}
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (!apiBaseUrl || !apiKey) {
|
|
290
|
-
throw new CliError(
|
|
291
|
-
"Missing credentials. Provide --base-url and --api-key, or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
|
|
292
|
-
);
|
|
108
|
+
function networkError(apiBaseUrl, error) {
|
|
109
|
+
if (error instanceof Error && error.message) {
|
|
110
|
+
return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
|
|
293
111
|
}
|
|
294
|
-
return {
|
|
295
|
-
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
296
|
-
apiKey
|
|
297
|
-
};
|
|
112
|
+
return `Could not reach PeakURL at ${apiBaseUrl}.`;
|
|
298
113
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
114
|
+
var ApiClient = class {
|
|
115
|
+
/**
|
|
116
|
+
* Creates a client bound to one resolved credential set.
|
|
117
|
+
*
|
|
118
|
+
* @param config Explicit API base URL plus bearer API key.
|
|
119
|
+
*/
|
|
120
|
+
constructor(config) {
|
|
121
|
+
this.config = config;
|
|
122
|
+
}
|
|
123
|
+
config;
|
|
124
|
+
/**
|
|
125
|
+
* Loads the currently authenticated user.
|
|
126
|
+
*
|
|
127
|
+
* PeakURL accepts bearer API keys on `GET /users/me`, which is also the
|
|
128
|
+
* CLI login verification flow.
|
|
129
|
+
*
|
|
130
|
+
* @returns API response envelope containing the authenticated user.
|
|
131
|
+
*/
|
|
132
|
+
whoami() {
|
|
133
|
+
return this.request("GET", "users/me");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Loads the current system status snapshot for the authenticated site.
|
|
137
|
+
*
|
|
138
|
+
* @returns API response envelope containing system status sections.
|
|
139
|
+
*/
|
|
140
|
+
getStatus() {
|
|
141
|
+
return this.request("GET", "system/status");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Creates a short URL.
|
|
145
|
+
*
|
|
146
|
+
* @param payload Request body accepted by `POST /api/v1/urls`.
|
|
147
|
+
* @returns API response envelope containing the created link.
|
|
148
|
+
*/
|
|
149
|
+
createUrl(payload) {
|
|
150
|
+
return this.request("POST", "urls", payload);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Lists short URLs with optional pagination and filtering.
|
|
154
|
+
*
|
|
155
|
+
* The current PeakURL app returns `{ items, meta }` under `data`, but the
|
|
156
|
+
* CLI keeps a slightly broader compatibility type for future-proofing.
|
|
157
|
+
*
|
|
158
|
+
* @param query Optional query-string values.
|
|
159
|
+
* @returns API response envelope containing list data.
|
|
160
|
+
*/
|
|
161
|
+
listUrls(query) {
|
|
162
|
+
return this.request("GET", "urls", void 0, query);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Exports the full accessible link dataset for the authenticated user.
|
|
166
|
+
*
|
|
167
|
+
* @param query Optional search and sort values.
|
|
168
|
+
* @returns API response envelope containing the full export payload.
|
|
169
|
+
*/
|
|
170
|
+
exportUrls(query) {
|
|
171
|
+
return this.request(
|
|
172
|
+
"GET",
|
|
173
|
+
"urls/export",
|
|
174
|
+
void 0,
|
|
175
|
+
query
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Imports multiple short links in one bulk request.
|
|
180
|
+
*
|
|
181
|
+
* @param payload Request body accepted by `POST /api/v1/urls/bulk`.
|
|
182
|
+
* @returns API response envelope containing created rows plus row errors.
|
|
183
|
+
*/
|
|
184
|
+
importUrls(payload) {
|
|
185
|
+
return this.request("POST", "urls/bulk", payload);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Loads a single short URL by identifier or alias.
|
|
189
|
+
*
|
|
190
|
+
* PeakURL resolves IDs, short codes, and aliases through the same route.
|
|
191
|
+
*
|
|
192
|
+
* @param idOrAlias Link identifier, short code, or alias.
|
|
193
|
+
* @returns API response envelope containing the resolved link.
|
|
194
|
+
*/
|
|
195
|
+
getUrl(idOrAlias) {
|
|
196
|
+
return this.request(
|
|
197
|
+
"GET",
|
|
198
|
+
`urls/${encodeURIComponent(idOrAlias)}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Deletes a short URL by its stable row ID.
|
|
203
|
+
*
|
|
204
|
+
* The current PeakURL backend delete route expects the row ID. The CLI can
|
|
205
|
+
* still accept an alias at the command layer by resolving it first.
|
|
206
|
+
*
|
|
207
|
+
* @param id Stable link row ID.
|
|
208
|
+
* @returns API response envelope containing the deletion result.
|
|
209
|
+
*/
|
|
210
|
+
deleteUrl(id) {
|
|
211
|
+
return this.request(
|
|
212
|
+
"DELETE",
|
|
213
|
+
`urls/${encodeURIComponent(id)}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Deletes multiple short URLs in a single bulk operation.
|
|
218
|
+
*
|
|
219
|
+
* @param ids Array of short URL IDs to delete.
|
|
220
|
+
* @returns API response envelope with deleted count.
|
|
221
|
+
*/
|
|
222
|
+
deleteUrlsBulk(ids) {
|
|
223
|
+
return this.request("DELETE", "urls/bulk", { ids });
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Deletes all accessible short URLs for the authenticated user.
|
|
227
|
+
*
|
|
228
|
+
* @returns API response envelope with deleted count.
|
|
229
|
+
*/
|
|
230
|
+
clearUrls() {
|
|
231
|
+
return this.request("DELETE", "urls");
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Empties all short links currently in trash.
|
|
235
|
+
*
|
|
236
|
+
* @returns API response envelope with deleted count.
|
|
237
|
+
*/
|
|
238
|
+
emptyTrash() {
|
|
239
|
+
return this.request("DELETE", "urls/trash");
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Lists audit log activity entries.
|
|
243
|
+
*
|
|
244
|
+
* @param query Optional query-string parameters for pagination or filters.
|
|
245
|
+
* @returns API response envelope containing activity items and meta.
|
|
246
|
+
*/
|
|
247
|
+
listActivity(query) {
|
|
248
|
+
return this.request(
|
|
249
|
+
"GET",
|
|
250
|
+
"analytics/activity",
|
|
251
|
+
void 0,
|
|
252
|
+
query
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Deletes a single audit log activity entry by its row ID.
|
|
257
|
+
*
|
|
258
|
+
* @param id Audit log row ID.
|
|
259
|
+
* @returns API response envelope confirming deletion.
|
|
260
|
+
*/
|
|
261
|
+
deleteActivity(id) {
|
|
262
|
+
return this.request(
|
|
263
|
+
"DELETE",
|
|
264
|
+
`analytics/activity/${encodeURIComponent(id)}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Deletes multiple audit log activity entries in bulk.
|
|
269
|
+
*
|
|
270
|
+
* @param ids Array of audit log row IDs.
|
|
271
|
+
* @returns API response envelope with deleted count.
|
|
272
|
+
*/
|
|
273
|
+
deleteActivityBulk(ids) {
|
|
274
|
+
return this.request(
|
|
275
|
+
"DELETE",
|
|
276
|
+
"analytics/activity/bulk",
|
|
277
|
+
{ ids }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Clears all audit log activity records.
|
|
282
|
+
*
|
|
283
|
+
* @returns API response envelope confirming all logs were deleted.
|
|
284
|
+
*/
|
|
285
|
+
clearActivity() {
|
|
286
|
+
return this.request("DELETE", "analytics/activity");
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Lists outbound webhooks for the authenticated user.
|
|
290
|
+
*
|
|
291
|
+
* @returns API response envelope containing webhook rows.
|
|
292
|
+
*/
|
|
293
|
+
listWebhooks() {
|
|
294
|
+
return this.request("GET", "webhooks");
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Creates one outbound webhook subscription.
|
|
298
|
+
*
|
|
299
|
+
* @param payload Request body accepted by `POST /api/v1/webhooks`.
|
|
300
|
+
* @returns API response envelope containing the created webhook.
|
|
301
|
+
*/
|
|
302
|
+
createWebhook(payload) {
|
|
303
|
+
return this.request("POST", "webhooks", payload);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Deletes one webhook by its stable row ID.
|
|
307
|
+
*
|
|
308
|
+
* @param id Webhook identifier returned by the list/create endpoints.
|
|
309
|
+
* @returns API response envelope containing the deletion result.
|
|
310
|
+
*/
|
|
311
|
+
deleteWebhook(id) {
|
|
312
|
+
return this.request(
|
|
313
|
+
"DELETE",
|
|
314
|
+
`webhooks/${encodeURIComponent(id)}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Performs one authenticated API request and normalizes the response.
|
|
319
|
+
*
|
|
320
|
+
* @param method HTTP method to send.
|
|
321
|
+
* @param path Route path relative to `/api/v1`.
|
|
322
|
+
* @param body Optional JSON body.
|
|
323
|
+
* @param query Optional query-string values.
|
|
324
|
+
* @returns Parsed PeakURL response envelope.
|
|
325
|
+
* @throws {CliError} When the network request fails or the API returns an error.
|
|
326
|
+
*/
|
|
327
|
+
async request(method, path, body, query) {
|
|
328
|
+
const url = buildApiUrl(this.config.apiBaseUrl, path, query);
|
|
329
|
+
let response;
|
|
330
|
+
try {
|
|
331
|
+
response = await fetch(url, {
|
|
332
|
+
method,
|
|
333
|
+
headers: {
|
|
334
|
+
Accept: "application/json",
|
|
335
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
336
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
337
|
+
},
|
|
338
|
+
body: body ? JSON.stringify(body) : void 0
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
|
|
342
|
+
cause: error instanceof Error ? error : void 0
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const rawText = await response.text();
|
|
346
|
+
if (!rawText) {
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
throw new CliError(
|
|
349
|
+
`PeakURL request failed with HTTP ${response.status}.`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
success: true,
|
|
354
|
+
message: "Request completed.",
|
|
355
|
+
data: void 0,
|
|
356
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
let parsed;
|
|
360
|
+
try {
|
|
361
|
+
parsed = JSON.parse(rawText);
|
|
362
|
+
} catch {
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
throw new CliError(
|
|
365
|
+
`PeakURL request failed with HTTP ${response.status}.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
throw new CliError("PeakURL returned an invalid JSON response.");
|
|
369
|
+
}
|
|
370
|
+
if (!isApiResponse(parsed)) {
|
|
371
|
+
throw new CliError(
|
|
372
|
+
"PeakURL returned an unexpected response envelope."
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (!response.ok || !parsed.success) {
|
|
376
|
+
const statusCode = response.status === 401 ? 2 : 1;
|
|
377
|
+
throw new CliError(
|
|
378
|
+
parsed.message || `PeakURL request failed with HTTP ${response.status}.`,
|
|
379
|
+
statusCode
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return parsed;
|
|
307
383
|
}
|
|
308
|
-
|
|
309
|
-
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
310
|
-
apiKey
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// src/lib/core.ts
|
|
315
|
-
import { createHash } from "crypto";
|
|
316
|
-
import {
|
|
317
|
-
chmod as chmod2,
|
|
318
|
-
copyFile,
|
|
319
|
-
lstat,
|
|
320
|
-
mkdir as mkdir2,
|
|
321
|
-
mkdtemp,
|
|
322
|
-
rm,
|
|
323
|
-
writeFile as writeFile2
|
|
324
|
-
} from "fs/promises";
|
|
325
|
-
import { tmpdir } from "os";
|
|
326
|
-
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
327
|
-
import { posix as pathPosix } from "path";
|
|
328
|
-
import { inflateRawSync } from "zlib";
|
|
384
|
+
};
|
|
329
385
|
|
|
330
386
|
// src/lib/output.ts
|
|
331
387
|
function writeStdout(message = "") {
|
|
@@ -455,14 +511,329 @@ function formatTable(headers, rows2, target = "stdout") {
|
|
|
455
511
|
)
|
|
456
512
|
].join("\n");
|
|
457
513
|
}
|
|
458
|
-
function formatDetailsTable(rows2, target = "stdout") {
|
|
459
|
-
return formatTable(["Detail", "Information"], rows2, target);
|
|
514
|
+
function formatDetailsTable(rows2, target = "stdout") {
|
|
515
|
+
return formatTable(["Detail", "Information"], rows2, target);
|
|
516
|
+
}
|
|
517
|
+
function writeJson(value) {
|
|
518
|
+
writeStdout(JSON.stringify(value, null, 2));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/lib/activity.ts
|
|
522
|
+
var ACTIVITY_LIST_KEYS = [
|
|
523
|
+
"items",
|
|
524
|
+
"results",
|
|
525
|
+
"activities",
|
|
526
|
+
"history"
|
|
527
|
+
];
|
|
528
|
+
function asObject(value) {
|
|
529
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
530
|
+
}
|
|
531
|
+
function asString(value) {
|
|
532
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
533
|
+
}
|
|
534
|
+
function asNumber(value) {
|
|
535
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
536
|
+
}
|
|
537
|
+
function truncate(value, maxLength) {
|
|
538
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
539
|
+
}
|
|
540
|
+
function getActivityMeta(data) {
|
|
541
|
+
const record = asObject(data);
|
|
542
|
+
if (!record) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const meta = asObject(record.meta);
|
|
546
|
+
if (meta) {
|
|
547
|
+
return {
|
|
548
|
+
page: asNumber(meta.page),
|
|
549
|
+
limit: asNumber(meta.limit),
|
|
550
|
+
totalItems: asNumber(meta.totalItems),
|
|
551
|
+
totalPages: asNumber(meta.totalPages)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
page: asNumber(record.page),
|
|
556
|
+
limit: asNumber(record.limit),
|
|
557
|
+
totalItems: asNumber(record.total),
|
|
558
|
+
totalPages: asNumber(record.totalPages)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function extractActivity(data) {
|
|
562
|
+
if (Array.isArray(data)) {
|
|
563
|
+
return data;
|
|
564
|
+
}
|
|
565
|
+
const record = asObject(data);
|
|
566
|
+
if (record) {
|
|
567
|
+
for (const key of ACTIVITY_LIST_KEYS) {
|
|
568
|
+
const value = record[key];
|
|
569
|
+
if (Array.isArray(value)) {
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
function formatActivityTable(items) {
|
|
577
|
+
if (items.length === 0) {
|
|
578
|
+
return "No activity logs found.";
|
|
579
|
+
}
|
|
580
|
+
const headers = [
|
|
581
|
+
"ID",
|
|
582
|
+
"Action",
|
|
583
|
+
"User",
|
|
584
|
+
"IP Address",
|
|
585
|
+
"Timestamp",
|
|
586
|
+
"Message"
|
|
587
|
+
];
|
|
588
|
+
const rows2 = items.map((item) => [
|
|
589
|
+
truncate(asString(item.id) || "-", 18),
|
|
590
|
+
truncate(asString(item.type) || "-", 18),
|
|
591
|
+
truncate(
|
|
592
|
+
asString(item.userName) || asString(item.userEmail) || "-",
|
|
593
|
+
18
|
|
594
|
+
),
|
|
595
|
+
truncate(asString(item.ipAddress) || "-", 16),
|
|
596
|
+
truncate(asString(item.createdAt) || "-", 22),
|
|
597
|
+
truncate(asString(item.message) || "-", 40)
|
|
598
|
+
]);
|
|
599
|
+
return formatTable(headers, rows2);
|
|
600
|
+
}
|
|
601
|
+
function formatActivitySummary(data, count) {
|
|
602
|
+
const meta = getActivityMeta(data);
|
|
603
|
+
if (!meta) {
|
|
604
|
+
return `${count} activity record${count === 1 ? "" : "s"} returned.`;
|
|
605
|
+
}
|
|
606
|
+
const total = meta.totalItems;
|
|
607
|
+
const page = meta.page;
|
|
608
|
+
const totalPages = meta.totalPages;
|
|
609
|
+
if (total !== void 0 && page !== void 0 && totalPages !== void 0) {
|
|
610
|
+
return `Page ${page} of ${totalPages}. ${total} total activity record${total === 1 ? "" : "s"}.`;
|
|
611
|
+
}
|
|
612
|
+
return `${count} activity record${count === 1 ? "" : "s"} returned.`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/config/store.ts
|
|
616
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
617
|
+
import { dirname, join } from "path";
|
|
618
|
+
import envPaths from "env-paths";
|
|
619
|
+
var CONFIG_FILENAME = "config.json";
|
|
620
|
+
var STATE_FILENAME = "state.json";
|
|
621
|
+
function getConfigPath() {
|
|
622
|
+
const paths = envPaths("peakurl", { suffix: "" });
|
|
623
|
+
const directory = process.platform === "darwin" ? paths.data : paths.config;
|
|
624
|
+
return join(directory, CONFIG_FILENAME);
|
|
625
|
+
}
|
|
626
|
+
function getStatePath() {
|
|
627
|
+
return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
|
|
628
|
+
}
|
|
629
|
+
async function ensureParentDir(filePath) {
|
|
630
|
+
const directory = dirname(filePath);
|
|
631
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
632
|
+
return directory;
|
|
633
|
+
}
|
|
634
|
+
var ConfigStore = class {
|
|
635
|
+
filePath;
|
|
636
|
+
/**
|
|
637
|
+
* Creates a config store bound to one on-disk file.
|
|
638
|
+
*
|
|
639
|
+
* @param filePath Optional override used by tests or advanced callers.
|
|
640
|
+
*/
|
|
641
|
+
constructor(filePath = getConfigPath()) {
|
|
642
|
+
this.filePath = filePath;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Loads the stored credential set from disk.
|
|
646
|
+
*
|
|
647
|
+
* Missing files are treated as "not configured yet" instead of as hard
|
|
648
|
+
* errors so first-run CLI flows remain clean.
|
|
649
|
+
*
|
|
650
|
+
* @returns Stored config or `null` when the file does not exist.
|
|
651
|
+
* @throws {CliError} When the file exists but is unreadable or invalid.
|
|
652
|
+
*/
|
|
653
|
+
async load() {
|
|
654
|
+
try {
|
|
655
|
+
const content = await readFile(this.filePath, "utf8");
|
|
656
|
+
const parsed = JSON.parse(content);
|
|
657
|
+
const apiBaseUrl = typeof parsed?.apiBaseUrl === "string" ? parsed.apiBaseUrl : typeof parsed?.baseUrl === "string" ? parsed.baseUrl : void 0;
|
|
658
|
+
if (typeof apiBaseUrl !== "string" || typeof parsed?.apiKey !== "string") {
|
|
659
|
+
throw new CliError(`Invalid config file: ${this.filePath}`);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
apiBaseUrl,
|
|
663
|
+
apiKey: parsed.apiKey
|
|
664
|
+
};
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code === "ENOENT") {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
if (error instanceof CliError) {
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
throw new CliError(
|
|
673
|
+
`Could not read PeakURL config at ${this.filePath}.`,
|
|
674
|
+
1,
|
|
675
|
+
{
|
|
676
|
+
cause: error instanceof Error ? error : void 0
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Persists one credential set to disk with restrictive file permissions.
|
|
683
|
+
*
|
|
684
|
+
* The chmod step is best-effort because Windows and some filesystems do not
|
|
685
|
+
* expose POSIX permission bits in the same way as Unix-like systems.
|
|
686
|
+
*
|
|
687
|
+
* @param config Normalized credential set to write.
|
|
688
|
+
*/
|
|
689
|
+
async save(config) {
|
|
690
|
+
const directory = await ensureParentDir(this.filePath);
|
|
691
|
+
await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
|
|
692
|
+
`, {
|
|
693
|
+
mode: 384
|
|
694
|
+
});
|
|
695
|
+
try {
|
|
696
|
+
await chmod(directory, 448);
|
|
697
|
+
await chmod(this.filePath, 384);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Removes the stored credential file.
|
|
703
|
+
*
|
|
704
|
+
* @returns `true` when a saved config file existed and was removed.
|
|
705
|
+
* @throws {CliError} When the file exists but cannot be removed.
|
|
706
|
+
*/
|
|
707
|
+
async clear() {
|
|
708
|
+
try {
|
|
709
|
+
await unlink(this.filePath);
|
|
710
|
+
return true;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code === "ENOENT") {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
throw new CliError(
|
|
716
|
+
`Could not remove PeakURL config at ${this.filePath}.`,
|
|
717
|
+
1,
|
|
718
|
+
{
|
|
719
|
+
cause: error instanceof Error ? error : void 0
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
var StateStore = class {
|
|
726
|
+
filePath;
|
|
727
|
+
/**
|
|
728
|
+
* Creates a state store bound to one on-disk file.
|
|
729
|
+
*
|
|
730
|
+
* @param filePath Optional override used by tests or advanced callers.
|
|
731
|
+
*/
|
|
732
|
+
constructor(filePath = getStatePath()) {
|
|
733
|
+
this.filePath = filePath;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Loads cached state from disk.
|
|
737
|
+
*
|
|
738
|
+
* Missing or invalid files are treated as empty state because the CLI can
|
|
739
|
+
* always recompute update metadata on the next successful network check.
|
|
740
|
+
*
|
|
741
|
+
* @returns Parsed state object or an empty object.
|
|
742
|
+
*/
|
|
743
|
+
async load() {
|
|
744
|
+
try {
|
|
745
|
+
const content = await readFile(this.filePath, "utf8");
|
|
746
|
+
const parsed = JSON.parse(content);
|
|
747
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
748
|
+
} catch {
|
|
749
|
+
return {};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Persists cached state to disk.
|
|
754
|
+
*
|
|
755
|
+
* @param state State payload to save.
|
|
756
|
+
*/
|
|
757
|
+
async save(state) {
|
|
758
|
+
const directory = await ensureParentDir(this.filePath);
|
|
759
|
+
await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
|
|
760
|
+
`, {
|
|
761
|
+
mode: 384
|
|
762
|
+
});
|
|
763
|
+
try {
|
|
764
|
+
await chmod(directory, 448);
|
|
765
|
+
await chmod(this.filePath, 384);
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// src/lib/auth.ts
|
|
772
|
+
var AUTH_REQUIRED_MESSAGE = "PeakURL credentials are not configured.";
|
|
773
|
+
var EXAMPLE_BASE_URL = "https://example.com/api/v1";
|
|
774
|
+
var EXAMPLE_API_KEY = "YOUR_API_KEY";
|
|
775
|
+
function authRows(commandName) {
|
|
776
|
+
const retryCommand = commandName ? `peakurl ${commandName}` : "peakurl whoami";
|
|
777
|
+
const rows2 = [
|
|
778
|
+
[
|
|
779
|
+
"Save credentials",
|
|
780
|
+
`peakurl login --base-url ${EXAMPLE_BASE_URL}
|
|
781
|
+
--api-key ${EXAMPLE_API_KEY}`,
|
|
782
|
+
"Regular use on this machine"
|
|
783
|
+
],
|
|
784
|
+
[
|
|
785
|
+
"Set environment variables",
|
|
786
|
+
`PEAKURL_BASE_URL=${EXAMPLE_BASE_URL}
|
|
787
|
+
PEAKURL_API_KEY=${EXAMPLE_API_KEY}`,
|
|
788
|
+
"CI, scripts, or one-off use"
|
|
789
|
+
],
|
|
790
|
+
["Then run", retryCommand, "After completing one of the steps above"]
|
|
791
|
+
];
|
|
792
|
+
return rows2;
|
|
793
|
+
}
|
|
794
|
+
function getLoginConfig(input, env) {
|
|
795
|
+
const apiBaseUrl = input.baseUrl?.trim() || env.PEAKURL_BASE_URL?.trim();
|
|
796
|
+
const apiKey = input.apiKey?.trim() || env.PEAKURL_API_KEY?.trim();
|
|
797
|
+
if (!apiBaseUrl || !apiKey) {
|
|
798
|
+
throw new CliError(
|
|
799
|
+
"Missing credentials. Provide --base-url and --api-key, or set PEAKURL_BASE_URL and PEAKURL_API_KEY."
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
804
|
+
apiKey
|
|
805
|
+
};
|
|
460
806
|
}
|
|
461
|
-
function
|
|
462
|
-
|
|
807
|
+
async function getAuthConfig(env, store = new ConfigStore()) {
|
|
808
|
+
const saved = await store.load();
|
|
809
|
+
const apiBaseUrl = env.PEAKURL_BASE_URL?.trim() || saved?.apiBaseUrl;
|
|
810
|
+
const apiKey = env.PEAKURL_API_KEY?.trim() || saved?.apiKey;
|
|
811
|
+
if (!apiBaseUrl || !apiKey) {
|
|
812
|
+
throw new CliError(AUTH_REQUIRED_MESSAGE, 1, {
|
|
813
|
+
kind: "auth_required"
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
apiBaseUrl: getApiBaseUrl(apiBaseUrl),
|
|
818
|
+
apiKey
|
|
819
|
+
};
|
|
463
820
|
}
|
|
464
821
|
|
|
465
822
|
// src/lib/core.ts
|
|
823
|
+
import { createHash } from "crypto";
|
|
824
|
+
import {
|
|
825
|
+
chmod as chmod2,
|
|
826
|
+
copyFile,
|
|
827
|
+
lstat,
|
|
828
|
+
mkdir as mkdir2,
|
|
829
|
+
mkdtemp,
|
|
830
|
+
rm,
|
|
831
|
+
writeFile as writeFile2
|
|
832
|
+
} from "fs/promises";
|
|
833
|
+
import { tmpdir } from "os";
|
|
834
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
835
|
+
import { posix as pathPosix } from "path";
|
|
836
|
+
import { inflateRawSync } from "zlib";
|
|
466
837
|
var DEFAULT_RELEASE_API_URL = "https://api.peakurl.org/v1/update";
|
|
467
838
|
var DEFAULT_CORE_PACKAGE_URL = "https://peakurl.org/latest.zip";
|
|
468
839
|
var EOCD_SIGNATURE = 101010256;
|
|
@@ -481,7 +852,7 @@ function getCorePackageUrl(env) {
|
|
|
481
852
|
"package download"
|
|
482
853
|
);
|
|
483
854
|
}
|
|
484
|
-
function
|
|
855
|
+
function asString2(value) {
|
|
485
856
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
486
857
|
}
|
|
487
858
|
function validateUrl(value, label) {
|
|
@@ -500,7 +871,7 @@ function validateUrl(value, label) {
|
|
|
500
871
|
return parsed.toString();
|
|
501
872
|
}
|
|
502
873
|
function normalizeSha256(value) {
|
|
503
|
-
const candidate =
|
|
874
|
+
const candidate = asString2(value)?.toLowerCase();
|
|
504
875
|
if (!candidate || !/^[a-f0-9]{64}$/.test(candidate)) {
|
|
505
876
|
throw new CliError(
|
|
506
877
|
"PeakURL release metadata is missing a valid SHA-256 checksum."
|
|
@@ -718,13 +1089,13 @@ async function getCoreRelease(env) {
|
|
|
718
1089
|
throw new CliError("PeakURL release metadata could not be loaded.");
|
|
719
1090
|
}
|
|
720
1091
|
const payload = await response.json();
|
|
721
|
-
const version =
|
|
1092
|
+
const version = asString2(payload.version) || "latest";
|
|
722
1093
|
return {
|
|
723
1094
|
version,
|
|
724
1095
|
downloadUrl: getCorePackageUrl(env),
|
|
725
1096
|
checksumSha256: normalizeSha256(payload.checksumSha256),
|
|
726
|
-
releasedAt:
|
|
727
|
-
releaseNotesUrl:
|
|
1097
|
+
releasedAt: asString2(payload.releasedAt),
|
|
1098
|
+
releaseNotesUrl: asString2(payload.releaseNotesUrl)
|
|
728
1099
|
};
|
|
729
1100
|
}
|
|
730
1101
|
async function downloadCorePackage(release, targetPath, force = false) {
|
|
@@ -793,17 +1164,23 @@ var EXPORT_HEADERS = [
|
|
|
793
1164
|
"created_at"
|
|
794
1165
|
];
|
|
795
1166
|
function text(value) {
|
|
796
|
-
|
|
1167
|
+
if (typeof value === "string") {
|
|
1168
|
+
return value;
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1171
|
+
return String(value);
|
|
1172
|
+
}
|
|
1173
|
+
return "";
|
|
797
1174
|
}
|
|
798
1175
|
function csvValue(value) {
|
|
799
|
-
const content = value
|
|
1176
|
+
const content = value === null || value === void 0 ? "" : text(value);
|
|
800
1177
|
if (/[",\r\n]/.test(content)) {
|
|
801
1178
|
return `"${content.replace(/"/g, '""')}"`;
|
|
802
1179
|
}
|
|
803
1180
|
return content;
|
|
804
1181
|
}
|
|
805
1182
|
function xmlValue(value) {
|
|
806
|
-
return
|
|
1183
|
+
return text(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
807
1184
|
}
|
|
808
1185
|
function aliasValue(link) {
|
|
809
1186
|
return text(link.alias) || text(link.shortCode);
|
|
@@ -1093,7 +1470,7 @@ function getImportFormat(filePath, value) {
|
|
|
1093
1470
|
);
|
|
1094
1471
|
}
|
|
1095
1472
|
async function readImportRows(filePath, format) {
|
|
1096
|
-
let textContent
|
|
1473
|
+
let textContent;
|
|
1097
1474
|
try {
|
|
1098
1475
|
textContent = await readFile2(filePath, "utf8");
|
|
1099
1476
|
} catch (error) {
|
|
@@ -1138,53 +1515,53 @@ function formatImportSummary(data) {
|
|
|
1138
1515
|
|
|
1139
1516
|
// src/lib/links.ts
|
|
1140
1517
|
var LIST_KEYS = ["urls", "items", "results"];
|
|
1141
|
-
function
|
|
1518
|
+
function asObject2(value) {
|
|
1142
1519
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1143
1520
|
}
|
|
1144
|
-
function
|
|
1521
|
+
function asString3(value) {
|
|
1145
1522
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1146
1523
|
}
|
|
1147
|
-
function
|
|
1524
|
+
function asNumber2(value) {
|
|
1148
1525
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1149
1526
|
}
|
|
1150
1527
|
function pickText(link, keys) {
|
|
1151
1528
|
for (const key of keys) {
|
|
1152
|
-
const value =
|
|
1529
|
+
const value = asString3(link[key]);
|
|
1153
1530
|
if (value) {
|
|
1154
1531
|
return value;
|
|
1155
1532
|
}
|
|
1156
1533
|
}
|
|
1157
1534
|
return void 0;
|
|
1158
1535
|
}
|
|
1159
|
-
function
|
|
1536
|
+
function truncate2(value, maxLength) {
|
|
1160
1537
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
1161
1538
|
}
|
|
1162
1539
|
function getListMeta(data) {
|
|
1163
|
-
const record =
|
|
1540
|
+
const record = asObject2(data);
|
|
1164
1541
|
if (!record) {
|
|
1165
1542
|
return null;
|
|
1166
1543
|
}
|
|
1167
|
-
const meta =
|
|
1544
|
+
const meta = asObject2(record.meta);
|
|
1168
1545
|
if (meta) {
|
|
1169
1546
|
return {
|
|
1170
|
-
page:
|
|
1171
|
-
limit:
|
|
1172
|
-
totalItems:
|
|
1173
|
-
totalPages:
|
|
1547
|
+
page: asNumber2(meta.page),
|
|
1548
|
+
limit: asNumber2(meta.limit),
|
|
1549
|
+
totalItems: asNumber2(meta.totalItems),
|
|
1550
|
+
totalPages: asNumber2(meta.totalPages)
|
|
1174
1551
|
};
|
|
1175
1552
|
}
|
|
1176
1553
|
return {
|
|
1177
|
-
page:
|
|
1178
|
-
limit:
|
|
1179
|
-
totalItems:
|
|
1180
|
-
totalPages:
|
|
1554
|
+
page: asNumber2(record.page),
|
|
1555
|
+
limit: asNumber2(record.limit),
|
|
1556
|
+
totalItems: asNumber2(record.total),
|
|
1557
|
+
totalPages: asNumber2(record.totalPages)
|
|
1181
1558
|
};
|
|
1182
1559
|
}
|
|
1183
1560
|
function extractLinks(data) {
|
|
1184
1561
|
if (Array.isArray(data)) {
|
|
1185
1562
|
return data;
|
|
1186
1563
|
}
|
|
1187
|
-
const record =
|
|
1564
|
+
const record = asObject2(data);
|
|
1188
1565
|
if (record) {
|
|
1189
1566
|
for (const key of LIST_KEYS) {
|
|
1190
1567
|
const value = record[key];
|
|
@@ -1221,14 +1598,14 @@ function formatLinkDetails(link) {
|
|
|
1221
1598
|
["Alias", getLinkAlias(link)],
|
|
1222
1599
|
["Short URL", getLinkShortUrl(link)],
|
|
1223
1600
|
["Destination", getLinkDestination(link)],
|
|
1224
|
-
["Title",
|
|
1225
|
-
["Status",
|
|
1601
|
+
["Title", asString3(link.title)],
|
|
1602
|
+
["Status", asString3(link.status)],
|
|
1226
1603
|
[
|
|
1227
1604
|
"Clicks",
|
|
1228
|
-
|
|
1605
|
+
asNumber2(link.clicks) === void 0 ? void 0 : String(link.clicks)
|
|
1229
1606
|
],
|
|
1230
|
-
["Created",
|
|
1231
|
-
["Updated",
|
|
1607
|
+
["Created", asString3(link.createdAt)],
|
|
1608
|
+
["Updated", asString3(link.updatedAt)]
|
|
1232
1609
|
].filter((entry) => Boolean(entry[1]));
|
|
1233
1610
|
if (rows2.length === 0) {
|
|
1234
1611
|
return "No link fields returned.";
|
|
@@ -1241,11 +1618,11 @@ function formatLinksTable(links) {
|
|
|
1241
1618
|
}
|
|
1242
1619
|
const headers = ["ID", "Alias", "Short URL", "Destination", "Status"];
|
|
1243
1620
|
const rows2 = links.map((link) => [
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1621
|
+
truncate2(getLinkId(link) || "-", 18),
|
|
1622
|
+
truncate2(getLinkAlias(link) || "-", 12),
|
|
1623
|
+
truncate2(getLinkShortUrl(link) || "-", 36),
|
|
1624
|
+
truncate2(getLinkDestination(link) || "-", 52),
|
|
1625
|
+
truncate2(asString3(link.status) || "-", 12)
|
|
1249
1626
|
]);
|
|
1250
1627
|
return formatTable(headers, rows2);
|
|
1251
1628
|
}
|
|
@@ -1527,6 +1904,33 @@ function locationRows(location) {
|
|
|
1527
1904
|
row("Download command", text3(location.downloadCommand), 60)
|
|
1528
1905
|
]);
|
|
1529
1906
|
}
|
|
1907
|
+
function cacheRows(cache) {
|
|
1908
|
+
if (!cache) {
|
|
1909
|
+
return [];
|
|
1910
|
+
}
|
|
1911
|
+
const redisEndpoint = cache.redis?.available || cache.redis?.configured ? `${text3(cache.redis.host) ?? "127.0.0.1"}:${formatCount(cache.redis.port) ?? "6379"}` : void 0;
|
|
1912
|
+
const redisStatus = cache.redis?.available ? cache.redis.serverVersion ? `Connected (v${cache.redis.serverVersion})` : "Connected" : cache.redis?.configured ? "Configured, unavailable" : void 0;
|
|
1913
|
+
const apcuStatus = cache.apcu?.available ? "Available" : cache.apcu?.extensionLoaded ? "Loaded, disabled" : cache.apcu !== void 0 && cache.apcu !== null ? "Missing" : void 0;
|
|
1914
|
+
return rows([
|
|
1915
|
+
row("Status", formatState(cache.status)),
|
|
1916
|
+
row("Enabled", yesNo(cache.enabled, "Enabled", "Disabled")),
|
|
1917
|
+
row("Active driver", text3(cache.activeDriver)),
|
|
1918
|
+
row("Configured driver", text3(cache.configuredDriver)),
|
|
1919
|
+
row("Cache size", formatSize(cache.sizeBytes)),
|
|
1920
|
+
row(
|
|
1921
|
+
cache.activeDriver === "redis" || cache.activeDriver === "apcu" ? "Cached items" : "Cached files",
|
|
1922
|
+
formatCount(cache.fileCount)
|
|
1923
|
+
),
|
|
1924
|
+
row("Default TTL", formatSeconds(cache.defaultTtl)),
|
|
1925
|
+
row("Negative TTL", formatSeconds(cache.negativeTtl)),
|
|
1926
|
+
row("Cache directory", text3(cache.path), 60),
|
|
1927
|
+
row("Directory exists", yesNo(cache.directoryExists)),
|
|
1928
|
+
row("Directory writable", yesNo(cache.writable)),
|
|
1929
|
+
row("Redis server", redisEndpoint),
|
|
1930
|
+
row("Redis status", redisStatus),
|
|
1931
|
+
row("APCu extension", apcuStatus)
|
|
1932
|
+
]);
|
|
1933
|
+
}
|
|
1530
1934
|
function dataRows(data) {
|
|
1531
1935
|
if (!data) {
|
|
1532
1936
|
return [];
|
|
@@ -1575,6 +1979,7 @@ ${checks}` : void 0,
|
|
|
1575
1979
|
section("Storage", storageRows(status2.storage)),
|
|
1576
1980
|
section("Mail", mailRows(status2.mail)),
|
|
1577
1981
|
section("Location", locationRows(status2.location)),
|
|
1982
|
+
section("Cache", cacheRows(status2.cache)),
|
|
1578
1983
|
section("Data", dataRows(status2.data))
|
|
1579
1984
|
].filter((value) => Boolean(value));
|
|
1580
1985
|
return sections.length > 0 ? sections.join("\n\n") : "No system status fields returned.";
|
|
@@ -1846,7 +2251,7 @@ function text5(value) {
|
|
|
1846
2251
|
function textList(value) {
|
|
1847
2252
|
return Array.isArray(value) ? value.map((item) => text5(item)).filter((item) => Boolean(item)) : [];
|
|
1848
2253
|
}
|
|
1849
|
-
function
|
|
2254
|
+
function truncate3(value, maxLength) {
|
|
1850
2255
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
1851
2256
|
}
|
|
1852
2257
|
function parseWebhookEvents(value, previous = []) {
|
|
@@ -1885,287 +2290,179 @@ function formatWebhookEventsTable() {
|
|
|
1885
2290
|
event.description
|
|
1886
2291
|
])
|
|
1887
2292
|
);
|
|
1888
|
-
}
|
|
1889
|
-
function formatWebhooksTable(webhooks) {
|
|
1890
|
-
if (webhooks.length === 0) {
|
|
1891
|
-
return "No webhooks found.";
|
|
1892
|
-
}
|
|
1893
|
-
return formatTable(
|
|
1894
|
-
["ID", "URL", "Events", "Status", "Secret"],
|
|
1895
|
-
webhooks.map((webhook) => [
|
|
1896
|
-
truncate2(getWebhookId(webhook) || "-", 18),
|
|
1897
|
-
truncate2(getWebhookUrl(webhook) || "-", 42),
|
|
1898
|
-
truncate2(getWebhookEvents(webhook).join(", ") || "-", 30),
|
|
1899
|
-
webhook.isActive === false ? "inactive" : "active",
|
|
1900
|
-
truncate2(text5(webhook.secretHint) || "-", 18)
|
|
1901
|
-
])
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
|
-
function formatWebhookDetails(webhook) {
|
|
1905
|
-
const rows2 = [
|
|
1906
|
-
["ID", getWebhookId(webhook)],
|
|
1907
|
-
["URL", getWebhookUrl(webhook)],
|
|
1908
|
-
[
|
|
1909
|
-
"Events",
|
|
1910
|
-
getWebhookEvents(webhook).length > 0 ? getWebhookEvents(webhook).join(", ") : void 0
|
|
1911
|
-
],
|
|
1912
|
-
["Status", webhook.isActive === false ? "inactive" : "active"],
|
|
1913
|
-
["Secret", text5(webhook.secret)],
|
|
1914
|
-
["Secret Hint", text5(webhook.secretHint)],
|
|
1915
|
-
["Created", text5(webhook.createdAt)]
|
|
1916
|
-
].filter((entry) => Boolean(entry[1]));
|
|
1917
|
-
if (rows2.length === 0) {
|
|
1918
|
-
return "No webhook fields returned.";
|
|
1919
|
-
}
|
|
1920
|
-
return formatDetailsTable(rows2);
|
|
1921
|
-
}
|
|
1922
|
-
function formatWebhooksSummary(webhooks) {
|
|
1923
|
-
return `${webhooks.length} webhook${webhooks.length === 1 ? "" : "s"} returned.`;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
// src/commands/core.ts
|
|
1927
|
-
async function downloadCore(options) {
|
|
1928
|
-
const release = await getCoreRelease(process.env);
|
|
1929
|
-
const result = await downloadCorePackage(
|
|
1930
|
-
release,
|
|
1931
|
-
cwd(),
|
|
1932
|
-
Boolean(options.force)
|
|
1933
|
-
);
|
|
1934
|
-
const responseBody = {
|
|
1935
|
-
success: true,
|
|
1936
|
-
message: "PeakURL downloaded.",
|
|
1937
|
-
data: result,
|
|
1938
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1939
|
-
};
|
|
1940
|
-
if (options.json) {
|
|
1941
|
-
writeJson(responseBody);
|
|
1942
|
-
return;
|
|
1943
|
-
}
|
|
1944
|
-
if (options.quiet) {
|
|
1945
|
-
writeStdout(result.path);
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
writeStdout(successLine(responseBody.message));
|
|
1949
|
-
writeStdout(formatCoreDownload(result));
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// src/commands/links.ts
|
|
1953
|
-
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1954
|
-
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
1955
|
-
|
|
1956
|
-
// src/api/client.ts
|
|
1957
|
-
function isApiResponse(value) {
|
|
1958
|
-
return Boolean(
|
|
1959
|
-
value && typeof value === "object" && "success" in value && "message" in value && "timestamp" in value
|
|
1960
|
-
);
|
|
1961
|
-
}
|
|
1962
|
-
function networkError(apiBaseUrl, error) {
|
|
1963
|
-
if (error instanceof Error && error.message) {
|
|
1964
|
-
return `Could not reach PeakURL at ${apiBaseUrl}. ${error.message}`;
|
|
1965
|
-
}
|
|
1966
|
-
return `Could not reach PeakURL at ${apiBaseUrl}.`;
|
|
1967
|
-
}
|
|
1968
|
-
var ApiClient = class {
|
|
1969
|
-
/**
|
|
1970
|
-
* Creates a client bound to one resolved credential set.
|
|
1971
|
-
*
|
|
1972
|
-
* @param config Explicit API base URL plus bearer API key.
|
|
1973
|
-
*/
|
|
1974
|
-
constructor(config) {
|
|
1975
|
-
this.config = config;
|
|
1976
|
-
}
|
|
1977
|
-
config;
|
|
1978
|
-
/**
|
|
1979
|
-
* Loads the currently authenticated user.
|
|
1980
|
-
*
|
|
1981
|
-
* PeakURL accepts bearer API keys on `GET /users/me`, which is also the
|
|
1982
|
-
* CLI login verification flow.
|
|
1983
|
-
*
|
|
1984
|
-
* @returns API response envelope containing the authenticated user.
|
|
1985
|
-
*/
|
|
1986
|
-
whoami() {
|
|
1987
|
-
return this.request("GET", "users/me");
|
|
1988
|
-
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Loads the current system status snapshot for the authenticated site.
|
|
1991
|
-
*
|
|
1992
|
-
* @returns API response envelope containing system status sections.
|
|
1993
|
-
*/
|
|
1994
|
-
getStatus() {
|
|
1995
|
-
return this.request("GET", "system/status");
|
|
1996
|
-
}
|
|
1997
|
-
/**
|
|
1998
|
-
* Creates a short URL.
|
|
1999
|
-
*
|
|
2000
|
-
* @param payload Request body accepted by `POST /api/v1/urls`.
|
|
2001
|
-
* @returns API response envelope containing the created link.
|
|
2002
|
-
*/
|
|
2003
|
-
createUrl(payload) {
|
|
2004
|
-
return this.request("POST", "urls", payload);
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* Lists short URLs with optional pagination and filtering.
|
|
2008
|
-
*
|
|
2009
|
-
* The current PeakURL app returns `{ items, meta }` under `data`, but the
|
|
2010
|
-
* CLI keeps a slightly broader compatibility type for future-proofing.
|
|
2011
|
-
*
|
|
2012
|
-
* @param query Optional query-string values.
|
|
2013
|
-
* @returns API response envelope containing list data.
|
|
2014
|
-
*/
|
|
2015
|
-
listUrls(query) {
|
|
2016
|
-
return this.request("GET", "urls", void 0, query);
|
|
2017
|
-
}
|
|
2018
|
-
/**
|
|
2019
|
-
* Exports the full accessible link dataset for the authenticated user.
|
|
2020
|
-
*
|
|
2021
|
-
* @param query Optional search and sort values.
|
|
2022
|
-
* @returns API response envelope containing the full export payload.
|
|
2023
|
-
*/
|
|
2024
|
-
exportUrls(query) {
|
|
2025
|
-
return this.request(
|
|
2026
|
-
"GET",
|
|
2027
|
-
"urls/export",
|
|
2028
|
-
void 0,
|
|
2029
|
-
query
|
|
2030
|
-
);
|
|
2031
|
-
}
|
|
2032
|
-
/**
|
|
2033
|
-
* Imports multiple short links in one bulk request.
|
|
2034
|
-
*
|
|
2035
|
-
* @param payload Request body accepted by `POST /api/v1/urls/bulk`.
|
|
2036
|
-
* @returns API response envelope containing created rows plus row errors.
|
|
2037
|
-
*/
|
|
2038
|
-
importUrls(payload) {
|
|
2039
|
-
return this.request("POST", "urls/bulk", payload);
|
|
2040
|
-
}
|
|
2041
|
-
/**
|
|
2042
|
-
* Loads a single short URL by identifier or alias.
|
|
2043
|
-
*
|
|
2044
|
-
* PeakURL resolves IDs, short codes, and aliases through the same route.
|
|
2045
|
-
*
|
|
2046
|
-
* @param idOrAlias Link identifier, short code, or alias.
|
|
2047
|
-
* @returns API response envelope containing the resolved link.
|
|
2048
|
-
*/
|
|
2049
|
-
getUrl(idOrAlias) {
|
|
2050
|
-
return this.request(
|
|
2051
|
-
"GET",
|
|
2052
|
-
`urls/${encodeURIComponent(idOrAlias)}`
|
|
2053
|
-
);
|
|
2293
|
+
}
|
|
2294
|
+
function formatWebhooksTable(webhooks) {
|
|
2295
|
+
if (webhooks.length === 0) {
|
|
2296
|
+
return "No webhooks found.";
|
|
2054
2297
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
)
|
|
2298
|
+
return formatTable(
|
|
2299
|
+
["ID", "URL", "Events", "Status", "Secret"],
|
|
2300
|
+
webhooks.map((webhook) => [
|
|
2301
|
+
truncate3(getWebhookId(webhook) || "-", 18),
|
|
2302
|
+
truncate3(getWebhookUrl(webhook) || "-", 42),
|
|
2303
|
+
truncate3(getWebhookEvents(webhook).join(", ") || "-", 30),
|
|
2304
|
+
webhook.isActive === false ? "inactive" : "active",
|
|
2305
|
+
truncate3(text5(webhook.secretHint) || "-", 18)
|
|
2306
|
+
])
|
|
2307
|
+
);
|
|
2308
|
+
}
|
|
2309
|
+
function formatWebhookDetails(webhook) {
|
|
2310
|
+
const rows2 = [
|
|
2311
|
+
["ID", getWebhookId(webhook)],
|
|
2312
|
+
["URL", getWebhookUrl(webhook)],
|
|
2313
|
+
[
|
|
2314
|
+
"Events",
|
|
2315
|
+
getWebhookEvents(webhook).length > 0 ? getWebhookEvents(webhook).join(", ") : void 0
|
|
2316
|
+
],
|
|
2317
|
+
["Status", webhook.isActive === false ? "inactive" : "active"],
|
|
2318
|
+
["Secret", text5(webhook.secret)],
|
|
2319
|
+
["Secret Hint", text5(webhook.secretHint)],
|
|
2320
|
+
["Created", text5(webhook.createdAt)]
|
|
2321
|
+
].filter((entry) => Boolean(entry[1]));
|
|
2322
|
+
if (rows2.length === 0) {
|
|
2323
|
+
return "No webhook fields returned.";
|
|
2069
2324
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2325
|
+
return formatDetailsTable(rows2);
|
|
2326
|
+
}
|
|
2327
|
+
function formatWebhooksSummary(webhooks) {
|
|
2328
|
+
return `${webhooks.length} webhook${webhooks.length === 1 ? "" : "s"} returned.`;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/commands/activity.ts
|
|
2332
|
+
async function listActivity(options) {
|
|
2333
|
+
const config = await getAuthConfig(process.env);
|
|
2334
|
+
const response = await new ApiClient(config).listActivity({
|
|
2335
|
+
page: options.page,
|
|
2336
|
+
limit: options.limit,
|
|
2337
|
+
search: options.search
|
|
2338
|
+
});
|
|
2339
|
+
const items = extractActivity(response.data);
|
|
2340
|
+
if (options.json) {
|
|
2341
|
+
writeJson(response);
|
|
2342
|
+
return;
|
|
2077
2343
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
return this.request("POST", "webhooks", payload);
|
|
2344
|
+
if (options.quiet) {
|
|
2345
|
+
for (const item of items) {
|
|
2346
|
+
if (item.id) {
|
|
2347
|
+
writeStdout(String(item.id));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return;
|
|
2086
2351
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2352
|
+
writeStdout(successLine(response.message || "Activity loaded."));
|
|
2353
|
+
writeStdout(formatActivityTable(items));
|
|
2354
|
+
writeStdout(formatActivitySummary(response.data, items.length));
|
|
2355
|
+
}
|
|
2356
|
+
async function deleteActivity(identifiers, options) {
|
|
2357
|
+
const config = await getAuthConfig(process.env);
|
|
2358
|
+
const client = new ApiClient(config);
|
|
2359
|
+
if (options.all) {
|
|
2360
|
+
const response2 = await client.clearActivity();
|
|
2361
|
+
if (options.json) {
|
|
2362
|
+
writeJson(response2);
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
if (options.quiet) {
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
writeStdout(
|
|
2369
|
+
successLine(response2.message || "All activity logs deleted.")
|
|
2097
2370
|
);
|
|
2371
|
+
return;
|
|
2098
2372
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
* @returns Parsed PeakURL response envelope.
|
|
2107
|
-
* @throws {CliError} When the network request fails or the API returns an error.
|
|
2108
|
-
*/
|
|
2109
|
-
async request(method, path, body, query) {
|
|
2110
|
-
const url = buildApiUrl(this.config.apiBaseUrl, path, query);
|
|
2111
|
-
let response;
|
|
2112
|
-
try {
|
|
2113
|
-
response = await fetch(url, {
|
|
2114
|
-
method,
|
|
2115
|
-
headers: {
|
|
2116
|
-
Accept: "application/json",
|
|
2117
|
-
Authorization: `Bearer ${this.config.apiKey}`,
|
|
2118
|
-
...body ? { "Content-Type": "application/json" } : {}
|
|
2119
|
-
},
|
|
2120
|
-
body: body ? JSON.stringify(body) : void 0
|
|
2121
|
-
});
|
|
2122
|
-
} catch (error) {
|
|
2123
|
-
throw new CliError(networkError(this.config.apiBaseUrl, error), 1, {
|
|
2124
|
-
cause: error instanceof Error ? error : void 0
|
|
2125
|
-
});
|
|
2126
|
-
}
|
|
2127
|
-
const rawText = await response.text();
|
|
2128
|
-
if (!rawText) {
|
|
2129
|
-
if (!response.ok) {
|
|
2130
|
-
throw new CliError(
|
|
2131
|
-
`PeakURL request failed with HTTP ${response.status}.`
|
|
2132
|
-
);
|
|
2373
|
+
const rawTargets = [];
|
|
2374
|
+
if (typeof identifiers === "string" && identifiers.trim()) {
|
|
2375
|
+
rawTargets.push(identifiers.trim());
|
|
2376
|
+
} else if (Array.isArray(identifiers)) {
|
|
2377
|
+
for (const item of identifiers) {
|
|
2378
|
+
if (typeof item === "string" && item.trim()) {
|
|
2379
|
+
rawTargets.push(item.trim());
|
|
2133
2380
|
}
|
|
2134
|
-
return {
|
|
2135
|
-
success: true,
|
|
2136
|
-
message: "Request completed.",
|
|
2137
|
-
data: void 0,
|
|
2138
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2139
|
-
};
|
|
2140
2381
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
if (
|
|
2146
|
-
|
|
2147
|
-
`PeakURL request failed with HTTP ${response.status}.`
|
|
2148
|
-
);
|
|
2382
|
+
}
|
|
2383
|
+
if (options.ids) {
|
|
2384
|
+
for (const id of options.ids.split(",")) {
|
|
2385
|
+
const trimmed = id.trim();
|
|
2386
|
+
if (trimmed) {
|
|
2387
|
+
rawTargets.push(trimmed);
|
|
2149
2388
|
}
|
|
2150
|
-
throw new CliError("PeakURL returned an invalid JSON response.");
|
|
2151
2389
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2390
|
+
}
|
|
2391
|
+
const uniqueTargets = Array.from(new Set(rawTargets));
|
|
2392
|
+
if (uniqueTargets.length === 0) {
|
|
2393
|
+
throw new CliError(
|
|
2394
|
+
"Specify one or more activity IDs to delete, or use --all to delete all activity logs."
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
if (uniqueTargets.length === 1) {
|
|
2398
|
+
const response2 = await client.deleteActivity(uniqueTargets[0]);
|
|
2399
|
+
if (options.json) {
|
|
2400
|
+
writeJson(response2);
|
|
2401
|
+
return;
|
|
2156
2402
|
}
|
|
2157
|
-
if (
|
|
2158
|
-
|
|
2159
|
-
throw new CliError(
|
|
2160
|
-
parsed.message || `PeakURL request failed with HTTP ${response.status}.`,
|
|
2161
|
-
statusCode
|
|
2162
|
-
);
|
|
2403
|
+
if (options.quiet) {
|
|
2404
|
+
return;
|
|
2163
2405
|
}
|
|
2164
|
-
|
|
2406
|
+
writeStdout(successLine(response2.message || "Activity log deleted."));
|
|
2407
|
+
return;
|
|
2165
2408
|
}
|
|
2166
|
-
|
|
2409
|
+
const response = await client.deleteActivityBulk(uniqueTargets);
|
|
2410
|
+
if (options.json) {
|
|
2411
|
+
writeJson(response);
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (options.quiet) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
writeStdout(
|
|
2418
|
+
successLine(
|
|
2419
|
+
response.message || `Deleted ${uniqueTargets.length} activity record${uniqueTargets.length === 1 ? "" : "s"}.`
|
|
2420
|
+
)
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
async function clearActivity(options) {
|
|
2424
|
+
const config = await getAuthConfig(process.env);
|
|
2425
|
+
const response = await new ApiClient(config).clearActivity();
|
|
2426
|
+
if (options.json) {
|
|
2427
|
+
writeJson(response);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
if (options.quiet) {
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
writeStdout(successLine(response.message || "All activity logs deleted."));
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// src/commands/core.ts
|
|
2437
|
+
import { cwd } from "process";
|
|
2438
|
+
async function downloadCore(options) {
|
|
2439
|
+
const release = await getCoreRelease(process.env);
|
|
2440
|
+
const result = await downloadCorePackage(
|
|
2441
|
+
release,
|
|
2442
|
+
cwd(),
|
|
2443
|
+
Boolean(options.force)
|
|
2444
|
+
);
|
|
2445
|
+
const responseBody = {
|
|
2446
|
+
success: true,
|
|
2447
|
+
message: "PeakURL downloaded.",
|
|
2448
|
+
data: result,
|
|
2449
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2450
|
+
};
|
|
2451
|
+
if (options.json) {
|
|
2452
|
+
writeJson(responseBody);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
if (options.quiet) {
|
|
2456
|
+
writeStdout(result.path);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
writeStdout(successLine(responseBody.message));
|
|
2460
|
+
writeStdout(formatCoreDownload(result));
|
|
2461
|
+
}
|
|
2167
2462
|
|
|
2168
2463
|
// src/commands/links.ts
|
|
2464
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
2465
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
2169
2466
|
function normalizeExpiresAt(value) {
|
|
2170
2467
|
if (!value) {
|
|
2171
2468
|
return void 0;
|
|
@@ -2308,17 +2605,93 @@ async function getLink(idOrAlias, options) {
|
|
|
2308
2605
|
writeStdout(successLine(response.message));
|
|
2309
2606
|
writeStdout(formatLinkDetails(response.data));
|
|
2310
2607
|
}
|
|
2311
|
-
async function deleteLink(
|
|
2608
|
+
async function deleteLink(identifiers, options) {
|
|
2312
2609
|
const config = await getAuthConfig(process.env);
|
|
2313
2610
|
const client = new ApiClient(config);
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2611
|
+
if (options.all) {
|
|
2612
|
+
const response2 = await client.clearUrls();
|
|
2613
|
+
if (options.json) {
|
|
2614
|
+
writeJson(response2);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (options.quiet) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
writeStdout(
|
|
2621
|
+
successLine(response2.message || "All short links deleted.")
|
|
2622
|
+
);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (options.trash || options.emptyTrash) {
|
|
2626
|
+
const response2 = await client.emptyTrash();
|
|
2627
|
+
if (options.json) {
|
|
2628
|
+
writeJson(response2);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (options.quiet) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
writeStdout(successLine(response2.message || "Trash emptied."));
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
const rawTargets = [];
|
|
2638
|
+
if (typeof identifiers === "string" && identifiers.trim()) {
|
|
2639
|
+
rawTargets.push(identifiers.trim());
|
|
2640
|
+
} else if (Array.isArray(identifiers)) {
|
|
2641
|
+
for (const item of identifiers) {
|
|
2642
|
+
if (typeof item === "string" && item.trim()) {
|
|
2643
|
+
rawTargets.push(item.trim());
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
if (options.ids) {
|
|
2648
|
+
for (const id of options.ids.split(",")) {
|
|
2649
|
+
const trimmed = id.trim();
|
|
2650
|
+
if (trimmed) {
|
|
2651
|
+
rawTargets.push(trimmed);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
const uniqueTargets = Array.from(new Set(rawTargets));
|
|
2656
|
+
if (uniqueTargets.length === 0) {
|
|
2317
2657
|
throw new CliError(
|
|
2318
|
-
"
|
|
2658
|
+
"Specify one or more link identifiers or aliases to delete, or use --all to delete all links."
|
|
2319
2659
|
);
|
|
2320
2660
|
}
|
|
2321
|
-
|
|
2661
|
+
if (uniqueTargets.length === 1) {
|
|
2662
|
+
const lookupResponse = await client.getUrl(uniqueTargets[0]);
|
|
2663
|
+
const resolvedId = getLinkId(lookupResponse.data);
|
|
2664
|
+
if (!resolvedId) {
|
|
2665
|
+
throw new CliError(
|
|
2666
|
+
"PeakURL returned a link record without an ID, so the CLI cannot delete it safely."
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
const response2 = await client.deleteUrl(resolvedId);
|
|
2670
|
+
if (options.json) {
|
|
2671
|
+
writeJson(response2);
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (options.quiet) {
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
writeStdout(successLine(response2.message));
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const resolvedIds = [];
|
|
2681
|
+
for (const target of uniqueTargets) {
|
|
2682
|
+
try {
|
|
2683
|
+
const lookup = await client.getUrl(target);
|
|
2684
|
+
const id = getLinkId(lookup.data);
|
|
2685
|
+
if (id) {
|
|
2686
|
+
resolvedIds.push(id);
|
|
2687
|
+
} else {
|
|
2688
|
+
resolvedIds.push(target);
|
|
2689
|
+
}
|
|
2690
|
+
} catch {
|
|
2691
|
+
resolvedIds.push(target);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
const response = await client.deleteUrlsBulk(resolvedIds);
|
|
2322
2695
|
if (options.json) {
|
|
2323
2696
|
writeJson(response);
|
|
2324
2697
|
return;
|
|
@@ -2326,7 +2699,11 @@ async function deleteLink(idOrAlias, options) {
|
|
|
2326
2699
|
if (options.quiet) {
|
|
2327
2700
|
return;
|
|
2328
2701
|
}
|
|
2329
|
-
writeStdout(
|
|
2702
|
+
writeStdout(
|
|
2703
|
+
successLine(
|
|
2704
|
+
response.message || `Deleted ${resolvedIds.length} short link${resolvedIds.length === 1 ? "" : "s"}.`
|
|
2705
|
+
)
|
|
2706
|
+
);
|
|
2330
2707
|
}
|
|
2331
2708
|
|
|
2332
2709
|
// src/commands/login.ts
|
|
@@ -2513,7 +2890,7 @@ async function deleteWebhook(id, options) {
|
|
|
2513
2890
|
}
|
|
2514
2891
|
writeStdout(successLine(response.message));
|
|
2515
2892
|
}
|
|
2516
|
-
|
|
2893
|
+
function listWebhookEvents(options) {
|
|
2517
2894
|
const response = {
|
|
2518
2895
|
success: true,
|
|
2519
2896
|
message: "Webhook events loaded.",
|
|
@@ -2584,7 +2961,7 @@ function getRetryCommandName(argv) {
|
|
|
2584
2961
|
if (!first || first.startsWith("-")) {
|
|
2585
2962
|
return void 0;
|
|
2586
2963
|
}
|
|
2587
|
-
if (first === "webhook" || first === "webhooks") {
|
|
2964
|
+
if (first === "webhook" || first === "webhooks" || first === "activity" || first === "activities") {
|
|
2588
2965
|
const second = argv[3]?.trim();
|
|
2589
2966
|
if (second && !second.startsWith("-")) {
|
|
2590
2967
|
return `${first} ${second}`;
|
|
@@ -2710,8 +3087,57 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
|
|
|
2710
3087
|
["peakurl get docs", "peakurl get url_123 --json"]
|
|
2711
3088
|
);
|
|
2712
3089
|
addExamples(
|
|
2713
|
-
program.command("delete").summary("Delete
|
|
2714
|
-
|
|
3090
|
+
program.command("delete").summary("Delete short links").description(
|
|
3091
|
+
"Delete PeakURL short links by ID or alias, in bulk, or clear all links."
|
|
3092
|
+
).helpOption("-h, --help", "Show help").argument("[id-or-alias...]", "Link identifier(s) or alias(es)").option("--all", "Delete all accessible short links").option(
|
|
3093
|
+
"--trash, --empty-trash",
|
|
3094
|
+
"Empty all short links currently in trash"
|
|
3095
|
+
).option(
|
|
3096
|
+
"--ids <ids>",
|
|
3097
|
+
"Comma-separated list of link IDs to bulk delete"
|
|
3098
|
+
).option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteLink),
|
|
3099
|
+
[
|
|
3100
|
+
"peakurl delete docs",
|
|
3101
|
+
"peakurl delete docs pricing launch",
|
|
3102
|
+
"peakurl delete --ids url_1,url_2,url_3",
|
|
3103
|
+
"peakurl delete --empty-trash",
|
|
3104
|
+
"peakurl delete --all"
|
|
3105
|
+
]
|
|
3106
|
+
);
|
|
3107
|
+
const activity = program.command("activity").alias("activities").summary("View and manage activity logs").helpOption("-h, --help", "Show help").description(
|
|
3108
|
+
"View audit log activity entries, delete specific records, or clear all history."
|
|
3109
|
+
);
|
|
3110
|
+
addExamples(activity, [
|
|
3111
|
+
"peakurl activity list",
|
|
3112
|
+
"peakurl activity delete act_123 act_456",
|
|
3113
|
+
"peakurl activity clear"
|
|
3114
|
+
]);
|
|
3115
|
+
addExamples(
|
|
3116
|
+
activity.command("list", { isDefault: true }).summary("List activity logs").description("List audit log activity records.").helpOption("-h, --help", "Show help").option("--page <number>", "Page number", parseNumber("page")).option("--limit <number>", "Page size", parseNumber("limit")).option("--search <query>", "Search term").option("--json", "Print machine-readable output").option("--quiet", "Print only activity record IDs").action(listActivity),
|
|
3117
|
+
[
|
|
3118
|
+
"peakurl activity",
|
|
3119
|
+
"peakurl activity list",
|
|
3120
|
+
"peakurl activity list --limit 25 --page 1",
|
|
3121
|
+
"peakurl activity list --search delete --json"
|
|
3122
|
+
]
|
|
3123
|
+
);
|
|
3124
|
+
addExamples(
|
|
3125
|
+
activity.command("delete").summary("Delete activity logs").description(
|
|
3126
|
+
"Delete one or more activity logs by ID, or clear all logs with --all."
|
|
3127
|
+
).helpOption("-h, --help", "Show help").argument("[ids...]", "One or more activity log IDs to delete").option("--all", "Delete all activity log history").option(
|
|
3128
|
+
"--ids <ids>",
|
|
3129
|
+
"Comma-separated list of activity log IDs to delete"
|
|
3130
|
+
).option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteActivity),
|
|
3131
|
+
[
|
|
3132
|
+
"peakurl activity delete act_123",
|
|
3133
|
+
"peakurl activity delete act_123 act_456",
|
|
3134
|
+
"peakurl activity delete --ids act_1,act_2",
|
|
3135
|
+
"peakurl activity delete --all"
|
|
3136
|
+
]
|
|
3137
|
+
);
|
|
3138
|
+
addExamples(
|
|
3139
|
+
activity.command("clear").summary("Clear all activity logs").description("Delete all audit log activity records permanently.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(clearActivity),
|
|
3140
|
+
["peakurl activity clear", "peakurl activity clear --json"]
|
|
2715
3141
|
);
|
|
2716
3142
|
addExamples(
|
|
2717
3143
|
program.command("update").summary("Check for CLI updates").description(
|
|
@@ -2719,18 +3145,25 @@ Run 'peakurl <command> --help' for command-specific flags and examples.`
|
|
|
2719
3145
|
).helpOption("-h, --help", "Show help").option(
|
|
2720
3146
|
"--check",
|
|
2721
3147
|
"Alias for checking update status without changing anything"
|
|
2722
|
-
).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action(
|
|
3148
|
+
).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action(
|
|
3149
|
+
(options) => checkUpdate(options, version)
|
|
3150
|
+
),
|
|
2723
3151
|
["peakurl update", "peakurl update --check", "peakurl update --json"]
|
|
2724
3152
|
);
|
|
2725
3153
|
const webhook = program.command("webhook").alias("webhooks").summary("Manage webhooks").helpOption("-h, --help", "Show help").description("Manage outbound webhook integrations.");
|
|
2726
3154
|
addExamples(webhook, [
|
|
3155
|
+
"peakurl webhook",
|
|
2727
3156
|
"peakurl webhook list",
|
|
2728
3157
|
"peakurl webhook create https://example.com/api/webhooks/peakurl --event link.clicked",
|
|
2729
3158
|
"peakurl webhook events"
|
|
2730
3159
|
]);
|
|
2731
3160
|
addExamples(
|
|
2732
|
-
webhook.command("list").summary("List webhooks").description("List outbound webhooks.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Print minimal webhook identifiers").action(listWebhooks),
|
|
2733
|
-
[
|
|
3161
|
+
webhook.command("list", { isDefault: true }).summary("List webhooks").description("List outbound webhooks.").helpOption("-h, --help", "Show help").option("--json", "Print machine-readable output").option("--quiet", "Print minimal webhook identifiers").action(listWebhooks),
|
|
3162
|
+
[
|
|
3163
|
+
"peakurl webhook",
|
|
3164
|
+
"peakurl webhook list",
|
|
3165
|
+
"peakurl webhook list --json"
|
|
3166
|
+
]
|
|
2734
3167
|
);
|
|
2735
3168
|
addExamples(
|
|
2736
3169
|
webhook.command("create").summary("Create a webhook").description("Create an outbound webhook.").helpOption("-h, --help", "Show help").argument("<url>", "Webhook endpoint URL").option(
|