@better-webhook/cli 0.2.0 → 0.3.1
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 +250 -86
- package/dist/index.cjs +746 -89
- package/dist/index.js +720 -91
- package/package.json +7 -3
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
3
25
|
|
|
4
26
|
// src/index.ts
|
|
27
|
+
var import_commander4 = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/webhooks.ts
|
|
5
30
|
var import_commander = require("commander");
|
|
31
|
+
var import_path2 = require("path");
|
|
6
32
|
var import_fs2 = require("fs");
|
|
7
|
-
var import_path = require("path");
|
|
8
33
|
|
|
9
|
-
// src/
|
|
34
|
+
// src/utils/index.ts
|
|
10
35
|
var import_fs = require("fs");
|
|
36
|
+
var import_path = require("path");
|
|
11
37
|
|
|
12
38
|
// src/schema.ts
|
|
13
39
|
var import_zod = require("zod");
|
|
@@ -43,24 +69,7 @@ ${issues}`);
|
|
|
43
69
|
return parsed.data;
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
// src/
|
|
47
|
-
function loadWebhookFile(path) {
|
|
48
|
-
let rawContent;
|
|
49
|
-
try {
|
|
50
|
-
rawContent = (0, import_fs.readFileSync)(path, "utf8");
|
|
51
|
-
} catch (e) {
|
|
52
|
-
throw new Error(`Failed to read file ${path}: ${e.message}`);
|
|
53
|
-
}
|
|
54
|
-
let json;
|
|
55
|
-
try {
|
|
56
|
-
json = JSON.parse(rawContent);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
|
|
59
|
-
}
|
|
60
|
-
return validateWebhookJSON(json, path);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/http.ts
|
|
72
|
+
// src/utils/http.ts
|
|
64
73
|
var import_undici = require("undici");
|
|
65
74
|
async function executeWebhook(def) {
|
|
66
75
|
const headerMap = {};
|
|
@@ -96,28 +105,232 @@ async function executeWebhook(def) {
|
|
|
96
105
|
};
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
// src/index.ts
|
|
100
|
-
var import_undici2 = require("undici");
|
|
101
|
-
var program = new import_commander.Command();
|
|
102
|
-
program.name("better-webhook").description("CLI for listing, downloading and executing predefined webhooks").version("0.2.0");
|
|
108
|
+
// src/utils/index.ts
|
|
103
109
|
function findWebhooksDir(cwd) {
|
|
104
110
|
return (0, import_path.resolve)(cwd, ".webhooks");
|
|
105
111
|
}
|
|
106
112
|
function listWebhookFiles(dir) {
|
|
113
|
+
return listJsonFiles(dir);
|
|
114
|
+
}
|
|
115
|
+
function findCapturesDir(cwd) {
|
|
116
|
+
return (0, import_path.resolve)(cwd, ".webhook-captures");
|
|
117
|
+
}
|
|
118
|
+
function listJsonFiles(dir) {
|
|
107
119
|
try {
|
|
108
|
-
const entries = (0,
|
|
120
|
+
const entries = (0, import_fs.readdirSync)(dir);
|
|
109
121
|
return entries.filter(
|
|
110
|
-
(e) => (0,
|
|
122
|
+
(e) => (0, import_fs.statSync)((0, import_path.join)(dir, e)).isFile() && (0, import_path.extname)(e) === ".json"
|
|
111
123
|
);
|
|
112
124
|
} catch {
|
|
113
125
|
return [];
|
|
114
126
|
}
|
|
115
127
|
}
|
|
128
|
+
function loadWebhookFile(path) {
|
|
129
|
+
let rawContent;
|
|
130
|
+
try {
|
|
131
|
+
rawContent = (0, import_fs.readFileSync)(path, "utf8");
|
|
132
|
+
} catch (e) {
|
|
133
|
+
throw new Error(`Failed to read file ${path}: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
let json;
|
|
136
|
+
try {
|
|
137
|
+
json = JSON.parse(rawContent);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
|
|
140
|
+
}
|
|
141
|
+
return validateWebhookJSON(json, path);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/commands/webhooks.ts
|
|
145
|
+
var import_fs3 = require("fs");
|
|
146
|
+
var import_undici2 = require("undici");
|
|
147
|
+
|
|
148
|
+
// src/config.ts
|
|
116
149
|
var TEMPLATE_REPO_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
|
|
117
150
|
var TEMPLATES = {
|
|
118
151
|
"stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
|
|
119
152
|
};
|
|
120
|
-
|
|
153
|
+
|
|
154
|
+
// src/commands/webhooks.ts
|
|
155
|
+
var import_prompts = __toESM(require("prompts"), 1);
|
|
156
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
157
|
+
function statExists(p) {
|
|
158
|
+
try {
|
|
159
|
+
(0, import_fs2.statSync)(p);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
var listCommand = new import_commander.Command().name("list").description("List available webhook JSON definitions in .webhooks directory").action(() => {
|
|
166
|
+
const cwd = process.cwd();
|
|
167
|
+
const dir = findWebhooksDir(cwd);
|
|
168
|
+
const files = listWebhookFiles(dir);
|
|
169
|
+
if (!files.length) {
|
|
170
|
+
console.log("\u{1F4ED} No webhook definitions found in .webhooks directory.");
|
|
171
|
+
console.log("\u{1F4A1} Create webhook files or download templates with:");
|
|
172
|
+
console.log(" better-webhook webhooks download --all");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log("Available webhook definitions:");
|
|
176
|
+
files.forEach((f) => console.log(` \u2022 ${(0, import_path2.basename)(f, ".json")}`));
|
|
177
|
+
});
|
|
178
|
+
var runCommand = new import_commander.Command().name("run").argument(
|
|
179
|
+
"[nameOrPath]",
|
|
180
|
+
"Webhook name (in .webhooks) or path to JSON file (optional - will prompt if not provided)"
|
|
181
|
+
).description(
|
|
182
|
+
"Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
|
|
183
|
+
).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options = {}) => {
|
|
184
|
+
const cwd = process.cwd();
|
|
185
|
+
const webhooksDir = findWebhooksDir(cwd);
|
|
186
|
+
let selectedNameOrPath = nameOrPath;
|
|
187
|
+
if (!selectedNameOrPath) {
|
|
188
|
+
const spinner = (0, import_ora.default)("Loading available webhooks...").start();
|
|
189
|
+
const localFiles = listWebhookFiles(webhooksDir);
|
|
190
|
+
const templates = Object.keys(TEMPLATES);
|
|
191
|
+
spinner.stop();
|
|
192
|
+
if (localFiles.length === 0 && templates.length === 0) {
|
|
193
|
+
console.log("\u{1F4ED} No webhook definitions found.");
|
|
194
|
+
console.log(
|
|
195
|
+
"\u{1F4A1} Create webhook files in .webhooks directory or download templates with:"
|
|
196
|
+
);
|
|
197
|
+
console.log(" better-webhook webhooks download --all");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const choices = [];
|
|
201
|
+
if (localFiles.length > 0) {
|
|
202
|
+
choices.push(
|
|
203
|
+
{
|
|
204
|
+
title: "--- Local Webhooks (.webhooks) ---",
|
|
205
|
+
description: "",
|
|
206
|
+
value: "",
|
|
207
|
+
type: "separator"
|
|
208
|
+
},
|
|
209
|
+
...localFiles.map((file) => ({
|
|
210
|
+
title: (0, import_path2.basename)(file, ".json"),
|
|
211
|
+
description: `Local file: ${file}`,
|
|
212
|
+
value: (0, import_path2.basename)(file, ".json"),
|
|
213
|
+
type: "local"
|
|
214
|
+
}))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (templates.length > 0) {
|
|
218
|
+
choices.push(
|
|
219
|
+
{
|
|
220
|
+
title: "--- Available Templates ---",
|
|
221
|
+
description: "",
|
|
222
|
+
value: "",
|
|
223
|
+
type: "separator"
|
|
224
|
+
},
|
|
225
|
+
...templates.map((template) => ({
|
|
226
|
+
title: template,
|
|
227
|
+
description: `Template: ${TEMPLATES[template]}`,
|
|
228
|
+
value: template,
|
|
229
|
+
type: "template"
|
|
230
|
+
}))
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const response = await (0, import_prompts.default)({
|
|
234
|
+
type: "select",
|
|
235
|
+
name: "webhook",
|
|
236
|
+
message: "Select a webhook to run:",
|
|
237
|
+
choices: choices.filter((choice) => choice.value !== ""),
|
|
238
|
+
// Remove separators for selection
|
|
239
|
+
initial: 0
|
|
240
|
+
});
|
|
241
|
+
if (!response.webhook) {
|
|
242
|
+
console.log("\u274C No webhook selected. Exiting.");
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
selectedNameOrPath = response.webhook;
|
|
247
|
+
if (selectedNameOrPath && templates.includes(selectedNameOrPath)) {
|
|
248
|
+
const downloadSpinner = (0, import_ora.default)(
|
|
249
|
+
`Downloading template: ${selectedNameOrPath}...`
|
|
250
|
+
).start();
|
|
251
|
+
try {
|
|
252
|
+
const rel = TEMPLATES[selectedNameOrPath];
|
|
253
|
+
const rawUrl = `${TEMPLATE_REPO_BASE}/${rel}`;
|
|
254
|
+
const { statusCode, body } = await (0, import_undici2.request)(rawUrl);
|
|
255
|
+
if (statusCode !== 200) {
|
|
256
|
+
throw new Error(`HTTP ${statusCode}`);
|
|
257
|
+
}
|
|
258
|
+
const text = await body.text();
|
|
259
|
+
const json = JSON.parse(text);
|
|
260
|
+
validateWebhookJSON(json, rawUrl);
|
|
261
|
+
(0, import_fs3.mkdirSync)(webhooksDir, { recursive: true });
|
|
262
|
+
const fileName = (0, import_path2.basename)(rel);
|
|
263
|
+
const destPath = (0, import_path2.join)(webhooksDir, fileName);
|
|
264
|
+
(0, import_fs3.writeFileSync)(destPath, JSON.stringify(json, null, 2));
|
|
265
|
+
selectedNameOrPath = (0, import_path2.basename)(fileName, ".json");
|
|
266
|
+
downloadSpinner.succeed(`Downloaded template: ${selectedNameOrPath}`);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
downloadSpinner.fail(`Failed to download template: ${error.message}`);
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!selectedNameOrPath) {
|
|
275
|
+
console.error("\u274C No webhook selected.");
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
let filePath;
|
|
280
|
+
if (selectedNameOrPath.endsWith(".json") && !selectedNameOrPath.includes("/") && !selectedNameOrPath.startsWith(".")) {
|
|
281
|
+
filePath = (0, import_path2.join)(webhooksDir, selectedNameOrPath);
|
|
282
|
+
} else {
|
|
283
|
+
const candidate = (0, import_path2.join)(
|
|
284
|
+
webhooksDir,
|
|
285
|
+
selectedNameOrPath + (selectedNameOrPath.endsWith(".json") ? "" : ".json")
|
|
286
|
+
);
|
|
287
|
+
if (statExists(candidate)) {
|
|
288
|
+
filePath = candidate;
|
|
289
|
+
} else {
|
|
290
|
+
filePath = (0, import_path2.resolve)(cwd, selectedNameOrPath);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!statExists(filePath)) {
|
|
294
|
+
console.error(`Webhook file not found: ${filePath}`);
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
let def;
|
|
299
|
+
try {
|
|
300
|
+
def = loadWebhookFile(filePath);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error(err.message);
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (options.url) def = { ...def, url: options.url };
|
|
307
|
+
if (options.method)
|
|
308
|
+
def = { ...def, method: options.method.toUpperCase() };
|
|
309
|
+
const executeSpinner = (0, import_ora.default)(
|
|
310
|
+
`Executing webhook: ${(0, import_path2.basename)(filePath, ".json")}...`
|
|
311
|
+
).start();
|
|
312
|
+
try {
|
|
313
|
+
const result = await executeWebhook(def);
|
|
314
|
+
executeSpinner.succeed("Webhook executed successfully!");
|
|
315
|
+
console.log("Status:", result.status);
|
|
316
|
+
console.log("Headers:");
|
|
317
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
318
|
+
console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
|
|
319
|
+
}
|
|
320
|
+
if (result.json !== void 0) {
|
|
321
|
+
console.log("Response JSON:");
|
|
322
|
+
console.log(JSON.stringify(result.json, null, 2));
|
|
323
|
+
} else {
|
|
324
|
+
console.log("Response Body:");
|
|
325
|
+
console.log(result.bodyText);
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
executeSpinner.fail("Request failed");
|
|
329
|
+
console.error("Error:", err.message);
|
|
330
|
+
process.exitCode = 1;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
var downloadCommand = new import_commander.Command().name("download").argument("[name]", "Template name to download").description(
|
|
121
334
|
"Download official webhook template(s) into the .webhooks directory. If no name is provided, prints available templates."
|
|
122
335
|
).option("-a, --all", "Download all available templates").option("-f, --force", "Overwrite existing files if they exist").action(
|
|
123
336
|
async (name, opts) => {
|
|
@@ -128,12 +341,12 @@ program.command("download [name]").description(
|
|
|
128
341
|
}
|
|
129
342
|
const cwd = process.cwd();
|
|
130
343
|
const dir = findWebhooksDir(cwd);
|
|
131
|
-
(0,
|
|
344
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
132
345
|
const toDownload = opts.all ? Object.keys(TEMPLATES) : name ? [name] : [];
|
|
133
346
|
if (!toDownload.length) {
|
|
134
347
|
console.log("Available templates:");
|
|
135
348
|
for (const key of Object.keys(TEMPLATES)) console.log(` - ${key}`);
|
|
136
|
-
console.log("Use: better-webhook download <name> OR --all");
|
|
349
|
+
console.log("Use: better-webhook webhooks download <name> OR --all");
|
|
137
350
|
return;
|
|
138
351
|
}
|
|
139
352
|
for (const templateName of toDownload) {
|
|
@@ -169,15 +382,15 @@ program.command("download [name]").description(
|
|
|
169
382
|
console.error(`Template failed schema validation: ${e.message}`);
|
|
170
383
|
continue;
|
|
171
384
|
}
|
|
172
|
-
const fileName = (0,
|
|
173
|
-
const destPath = (0,
|
|
174
|
-
if ((0,
|
|
385
|
+
const fileName = (0, import_path2.basename)(rel);
|
|
386
|
+
const destPath = (0, import_path2.join)(dir, fileName);
|
|
387
|
+
if ((0, import_fs3.existsSync)(destPath) && !opts.force) {
|
|
175
388
|
console.log(
|
|
176
389
|
`Skipping existing file ${fileName} (use --force to overwrite)`
|
|
177
390
|
);
|
|
178
391
|
continue;
|
|
179
392
|
}
|
|
180
|
-
(0,
|
|
393
|
+
(0, import_fs3.writeFileSync)(destPath, JSON.stringify(json, null, 2));
|
|
181
394
|
console.log(`Downloaded ${templateName} -> .webhooks/${fileName}`);
|
|
182
395
|
} catch (e) {
|
|
183
396
|
console.error(`Error downloading ${templateName}: ${e.message}`);
|
|
@@ -185,75 +398,519 @@ program.command("download [name]").description(
|
|
|
185
398
|
}
|
|
186
399
|
}
|
|
187
400
|
);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
401
|
+
var webhooks = new import_commander.Command().name("webhooks").description("Manage and execute webhook definitions").addCommand(listCommand).addCommand(runCommand).addCommand(downloadCommand);
|
|
402
|
+
|
|
403
|
+
// src/commands/capture.ts
|
|
404
|
+
var import_commander2 = require("commander");
|
|
405
|
+
var import_fs6 = require("fs");
|
|
406
|
+
var import_path5 = require("path");
|
|
407
|
+
|
|
408
|
+
// src/capture.ts
|
|
409
|
+
var import_http2 = require("http");
|
|
410
|
+
var import_fs4 = require("fs");
|
|
411
|
+
var import_path3 = require("path");
|
|
412
|
+
var import_crypto = require("crypto");
|
|
413
|
+
var WebhookCaptureServer = class {
|
|
414
|
+
server = null;
|
|
415
|
+
capturesDir;
|
|
416
|
+
constructor(capturesDir) {
|
|
417
|
+
this.capturesDir = capturesDir;
|
|
418
|
+
if (!(0, import_fs4.existsSync)(capturesDir)) {
|
|
419
|
+
(0, import_fs4.mkdirSync)(capturesDir, { recursive: true });
|
|
420
|
+
}
|
|
195
421
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
422
|
+
start(startPort = 3001, maxAttempts = 20) {
|
|
423
|
+
return new Promise((resolve4, reject) => {
|
|
424
|
+
if (!Number.isInteger(startPort) || startPort < 0 || startPort > 65535) {
|
|
425
|
+
startPort = 3001;
|
|
426
|
+
}
|
|
427
|
+
let attempt = 0;
|
|
428
|
+
const tryListen = (portToTry) => {
|
|
429
|
+
this.server = (0, import_http2.createServer)(
|
|
430
|
+
async (req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
await this.handleRequest(req, res);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error("Error handling request:", error);
|
|
435
|
+
res.statusCode = 500;
|
|
436
|
+
res.end("Internal Server Error");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
const onError = (err) => {
|
|
441
|
+
this.server?.off("error", onError);
|
|
442
|
+
this.server?.off("listening", onListening);
|
|
443
|
+
if (err && err.code === "EADDRINUSE") {
|
|
444
|
+
attempt += 1;
|
|
445
|
+
if (startPort === 0) {
|
|
446
|
+
reject(new Error("Failed to bind to an ephemeral port."));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (attempt >= maxAttempts) {
|
|
450
|
+
reject(
|
|
451
|
+
new Error(
|
|
452
|
+
`All ${maxAttempts} port attempts starting at ${startPort} are in use.`
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const nextPort = startPort + attempt;
|
|
458
|
+
tryListen(nextPort);
|
|
459
|
+
} else {
|
|
460
|
+
reject(err);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
const onListening = () => {
|
|
464
|
+
this.server?.off("error", onError);
|
|
465
|
+
this.server?.off("listening", onListening);
|
|
466
|
+
const address = this.server?.address();
|
|
467
|
+
const actualPort = address?.port ?? portToTry;
|
|
468
|
+
console.log(
|
|
469
|
+
`\u{1F3A3} Webhook capture server running on http://localhost:${actualPort}`
|
|
470
|
+
);
|
|
471
|
+
console.log(
|
|
472
|
+
`\u{1F4C1} Captured webhooks will be saved to: ${this.capturesDir}`
|
|
473
|
+
);
|
|
474
|
+
console.log(
|
|
475
|
+
"\u{1F4A1} Send webhooks to any path on this server to capture them"
|
|
476
|
+
);
|
|
477
|
+
console.log("\u23F9\uFE0F Press Ctrl+C to stop the server");
|
|
478
|
+
resolve4(actualPort);
|
|
479
|
+
};
|
|
480
|
+
this.server.on("error", onError);
|
|
481
|
+
this.server.on("listening", onListening);
|
|
482
|
+
this.server.listen(portToTry);
|
|
483
|
+
};
|
|
484
|
+
tryListen(startPort === 0 ? 0 : startPort);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
stop() {
|
|
488
|
+
return new Promise((resolve4) => {
|
|
489
|
+
if (this.server) {
|
|
490
|
+
this.server.close(() => {
|
|
491
|
+
console.log("\u{1F4F4} Webhook capture server stopped");
|
|
492
|
+
resolve4();
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
resolve4();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async handleRequest(req, res) {
|
|
500
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
501
|
+
const id = this.generateId();
|
|
502
|
+
const url = req.url || "/";
|
|
503
|
+
const urlParts = new URL(url, `http://${req.headers.host || "localhost"}`);
|
|
504
|
+
const query = {};
|
|
505
|
+
for (const [key, value] of urlParts.searchParams.entries()) {
|
|
506
|
+
if (query[key]) {
|
|
507
|
+
if (Array.isArray(query[key])) {
|
|
508
|
+
query[key].push(value);
|
|
509
|
+
} else {
|
|
510
|
+
query[key] = [query[key], value];
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
query[key] = value;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const chunks = [];
|
|
517
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
518
|
+
await new Promise((resolve4) => {
|
|
519
|
+
req.on("end", () => resolve4());
|
|
520
|
+
});
|
|
521
|
+
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
522
|
+
let parsedBody;
|
|
523
|
+
try {
|
|
524
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : null;
|
|
525
|
+
} catch {
|
|
526
|
+
parsedBody = rawBody || null;
|
|
527
|
+
}
|
|
528
|
+
const captured = {
|
|
529
|
+
id,
|
|
530
|
+
timestamp,
|
|
531
|
+
method: req.method || "GET",
|
|
532
|
+
url: urlParts.pathname,
|
|
533
|
+
headers: req.headers,
|
|
534
|
+
body: parsedBody,
|
|
535
|
+
rawBody,
|
|
536
|
+
query
|
|
537
|
+
};
|
|
538
|
+
const filename = `${timestamp.replace(/[:.]/g, "-")}_${id}.json`;
|
|
539
|
+
const filepath = (0, import_path3.join)(this.capturesDir, filename);
|
|
540
|
+
try {
|
|
541
|
+
(0, import_fs4.writeFileSync)(filepath, JSON.stringify(captured, null, 2));
|
|
542
|
+
console.log(
|
|
543
|
+
`\u{1F4E6} Captured ${req.method} ${urlParts.pathname} -> ${filename}`
|
|
544
|
+
);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error(`\u274C Failed to save capture: ${error}`);
|
|
547
|
+
}
|
|
548
|
+
res.statusCode = 200;
|
|
549
|
+
res.setHeader("Content-Type", "application/json");
|
|
550
|
+
res.end(
|
|
551
|
+
JSON.stringify(
|
|
552
|
+
{
|
|
553
|
+
message: "Webhook captured successfully",
|
|
554
|
+
id,
|
|
555
|
+
timestamp,
|
|
556
|
+
file: filename
|
|
557
|
+
},
|
|
558
|
+
null,
|
|
559
|
+
2
|
|
560
|
+
)
|
|
209
561
|
);
|
|
210
|
-
|
|
211
|
-
|
|
562
|
+
}
|
|
563
|
+
generateId() {
|
|
564
|
+
try {
|
|
565
|
+
return (0, import_crypto.randomUUID)();
|
|
566
|
+
} catch {
|
|
567
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/replay.ts
|
|
573
|
+
var import_fs5 = require("fs");
|
|
574
|
+
var import_path4 = require("path");
|
|
575
|
+
var WebhookReplayer = class {
|
|
576
|
+
constructor(capturesDir) {
|
|
577
|
+
this.capturesDir = capturesDir;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* List all captured webhooks
|
|
581
|
+
*/
|
|
582
|
+
listCaptured() {
|
|
583
|
+
try {
|
|
584
|
+
const files = listJsonFiles(this.capturesDir).sort().reverse();
|
|
585
|
+
return files.map((file) => ({
|
|
586
|
+
file,
|
|
587
|
+
capture: this.loadCapture((0, import_path4.join)(this.capturesDir, file))
|
|
588
|
+
}));
|
|
589
|
+
} catch {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Load a specific captured webhook by filename or ID
|
|
595
|
+
*/
|
|
596
|
+
loadCapture(filePathOrId) {
|
|
597
|
+
let filepath;
|
|
598
|
+
if (filePathOrId.includes("/") || filePathOrId.endsWith(".json")) {
|
|
599
|
+
filepath = filePathOrId;
|
|
212
600
|
} else {
|
|
213
|
-
|
|
601
|
+
const files = listJsonFiles(this.capturesDir).filter(
|
|
602
|
+
(f) => f.includes(filePathOrId)
|
|
603
|
+
);
|
|
604
|
+
if (files.length === 0) {
|
|
605
|
+
throw new Error(`No capture found with ID: ${filePathOrId}`);
|
|
606
|
+
}
|
|
607
|
+
if (files.length > 1) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`Multiple captures found with ID ${filePathOrId}: ${files.join(", ")}`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
filepath = (0, import_path4.join)(this.capturesDir, files[0] ?? "");
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const content = (0, import_fs5.readFileSync)(filepath, "utf8");
|
|
616
|
+
return JSON.parse(content);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Failed to load capture from ${filepath}: ${error.message}`
|
|
620
|
+
);
|
|
214
621
|
}
|
|
215
622
|
}
|
|
216
|
-
|
|
217
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Replay a captured webhook to a target URL
|
|
625
|
+
*/
|
|
626
|
+
async replay(captureId, targetUrl, options = {}) {
|
|
627
|
+
const capture2 = this.loadCapture(captureId);
|
|
628
|
+
const webhookDef = {
|
|
629
|
+
url: options.url || targetUrl,
|
|
630
|
+
method: options.method || capture2.method,
|
|
631
|
+
headers: options.headers || this.convertHeaders(capture2.headers),
|
|
632
|
+
body: capture2.body
|
|
633
|
+
};
|
|
634
|
+
console.log(`\u{1F504} Replaying webhook ${capture2.id} (${capture2.timestamp})`);
|
|
635
|
+
console.log(` Method: ${webhookDef.method}`);
|
|
636
|
+
console.log(` URL: ${webhookDef.url}`);
|
|
637
|
+
console.log(` Original: ${capture2.method} ${capture2.url}`);
|
|
638
|
+
try {
|
|
639
|
+
const result = await executeWebhook(webhookDef);
|
|
640
|
+
return result;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
throw new Error(`Replay failed: ${error.message}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Convert captured webhook to template format
|
|
647
|
+
*/
|
|
648
|
+
captureToTemplate(captureId, templateUrl) {
|
|
649
|
+
const capture2 = this.loadCapture(captureId);
|
|
650
|
+
return {
|
|
651
|
+
url: templateUrl || "http://localhost:3000/webhook",
|
|
652
|
+
method: capture2.method,
|
|
653
|
+
headers: this.convertHeaders(capture2.headers),
|
|
654
|
+
body: capture2.body
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Convert captured headers to template format
|
|
659
|
+
*/
|
|
660
|
+
convertHeaders(headers) {
|
|
661
|
+
const result = [];
|
|
662
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
663
|
+
const skipHeaders = [
|
|
664
|
+
"host",
|
|
665
|
+
"content-length",
|
|
666
|
+
"connection",
|
|
667
|
+
"accept-encoding",
|
|
668
|
+
"user-agent",
|
|
669
|
+
"x-forwarded-for",
|
|
670
|
+
"x-forwarded-proto"
|
|
671
|
+
];
|
|
672
|
+
if (skipHeaders.includes(key.toLowerCase())) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (Array.isArray(value)) {
|
|
676
|
+
if (value.length === 1) {
|
|
677
|
+
result.push({ key, value: value[0] ?? "" });
|
|
678
|
+
} else {
|
|
679
|
+
result.push({ key, value: value.join(", ") });
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
result.push({ key, value });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get summary information about a capture
|
|
689
|
+
*/
|
|
690
|
+
getCaptureSummary(captureId) {
|
|
691
|
+
const capture2 = this.loadCapture(captureId);
|
|
692
|
+
const date = new Date(capture2.timestamp);
|
|
693
|
+
const bodySize = capture2.rawBody ? capture2.rawBody.length : 0;
|
|
694
|
+
const headerCount = Object.keys(capture2.headers).length;
|
|
695
|
+
return [
|
|
696
|
+
`ID: ${capture2.id}`,
|
|
697
|
+
`Date: ${date.toLocaleString()}`,
|
|
698
|
+
`Method: ${capture2.method}`,
|
|
699
|
+
`Path: ${capture2.url}`,
|
|
700
|
+
`Headers: ${headerCount}`,
|
|
701
|
+
`Body Size: ${bodySize} bytes`,
|
|
702
|
+
`Query Params: ${Object.keys(capture2.query).length}`
|
|
703
|
+
].join("\n");
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/commands/capture.ts
|
|
708
|
+
var listCommand2 = new import_commander2.Command().name("list").description("List captured webhook requests").option("-l, --limit <limit>", "Maximum number of captures to show", "10").action((options) => {
|
|
709
|
+
const cwd = process.cwd();
|
|
710
|
+
const capturesDir = findCapturesDir(cwd);
|
|
711
|
+
const replayer = new WebhookReplayer(capturesDir);
|
|
712
|
+
const captures = replayer.listCaptured();
|
|
713
|
+
const limit = parseInt(options.limit, 10);
|
|
714
|
+
if (Number.isNaN(limit) || limit <= 0) {
|
|
715
|
+
console.error("Invalid --limit: must be a positive integer.");
|
|
218
716
|
process.exitCode = 1;
|
|
219
717
|
return;
|
|
220
718
|
}
|
|
221
|
-
|
|
719
|
+
if (captures.length === 0) {
|
|
720
|
+
console.log("No webhook captures found.");
|
|
721
|
+
console.log(`Run 'better-webhook capture' to start capturing webhooks.`);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
console.log(
|
|
725
|
+
`\u{1F4CB} Found ${captures.length} captured webhooks (showing ${Math.min(limit, captures.length)}):
|
|
726
|
+
`
|
|
727
|
+
);
|
|
728
|
+
captures.slice(0, limit).forEach(({ file, capture: capture2 }) => {
|
|
729
|
+
const date = new Date(capture2.timestamp).toLocaleString();
|
|
730
|
+
const bodySize = capture2.rawBody ? capture2.rawBody.length : 0;
|
|
731
|
+
console.log(`\u{1F194} ${capture2.id} | \u{1F4C5} ${date}`);
|
|
732
|
+
console.log(` ${capture2.method} ${capture2.url} | ${bodySize} bytes`);
|
|
733
|
+
console.log(` \u{1F4C4} ${file}
|
|
734
|
+
`);
|
|
735
|
+
});
|
|
736
|
+
if (captures.length > limit) {
|
|
737
|
+
console.log(
|
|
738
|
+
`... and ${captures.length - limit} more. Use --limit to show more.`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
var templateCommand = new import_commander2.Command().name("template").argument("<captureId>", "ID of the captured webhook to create template from").argument("[templateName]", "Name for the generated template").description("Generate a webhook template from a captured request").option(
|
|
743
|
+
"-u, --url <url>",
|
|
744
|
+
"Template URL (default: http://localhost:3000/webhook)"
|
|
745
|
+
).option("-o, --output-dir <dir>", "Output directory (default: .webhooks)").action(
|
|
746
|
+
async (captureId, templateName, options) => {
|
|
747
|
+
const cwd = process.cwd();
|
|
748
|
+
const capturesDir = findCapturesDir(cwd);
|
|
749
|
+
const replayer = new WebhookReplayer(capturesDir);
|
|
750
|
+
try {
|
|
751
|
+
const template = replayer.captureToTemplate(captureId, options?.url);
|
|
752
|
+
if (!templateName) {
|
|
753
|
+
const capture2 = replayer.loadCapture(captureId);
|
|
754
|
+
const date = new Date(capture2.timestamp).toISOString().split("T")[0];
|
|
755
|
+
const pathPart = capture2.url.replace(/[^a-zA-Z0-9]/g, "_").substring(1) || "webhook";
|
|
756
|
+
templateName = `captured_${date}_${pathPart}_${capture2.id}`;
|
|
757
|
+
}
|
|
758
|
+
const outputDir = options?.outputDir ? (0, import_path5.resolve)(cwd, options.outputDir) : findWebhooksDir(cwd);
|
|
759
|
+
(0, import_fs6.mkdirSync)(outputDir, { recursive: true });
|
|
760
|
+
const templatePath = (0, import_path5.join)(outputDir, `${templateName}.json`);
|
|
761
|
+
(0, import_fs6.writeFileSync)(templatePath, JSON.stringify(template, null, 2));
|
|
762
|
+
console.log(`\u2705 Template created: ${templatePath}`);
|
|
763
|
+
console.log(
|
|
764
|
+
`\u{1F504} Run it with: better-webhook webhooks run ${templateName}`
|
|
765
|
+
);
|
|
766
|
+
console.log("\n\u{1F4CA} Template Summary:");
|
|
767
|
+
console.log(replayer.getCaptureSummary(captureId));
|
|
768
|
+
} catch (error) {
|
|
769
|
+
console.error("\u274C Template generation failed:", error.message);
|
|
770
|
+
process.exitCode = 1;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
var capture = new import_commander2.Command().name("capture").description(
|
|
775
|
+
"Start a server to capture incoming webhook requests, or list captured webhooks"
|
|
776
|
+
).option("-p, --port <port>", "Port to listen on", "3001").action(async (options) => {
|
|
777
|
+
const cwd = process.cwd();
|
|
778
|
+
const capturesDir = findCapturesDir(cwd);
|
|
779
|
+
const server = new WebhookCaptureServer(capturesDir);
|
|
780
|
+
let actualPort;
|
|
222
781
|
try {
|
|
223
|
-
|
|
224
|
-
} catch (
|
|
225
|
-
console.error(
|
|
782
|
+
actualPort = await server.start(parseInt(options.port));
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error("Failed to start capture server:", error.message);
|
|
226
785
|
process.exitCode = 1;
|
|
227
786
|
return;
|
|
228
787
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
788
|
+
const shutdown = async () => {
|
|
789
|
+
console.log("\n\u{1F6D1} Shutting down server...");
|
|
790
|
+
await server.stop();
|
|
791
|
+
process.exit(0);
|
|
792
|
+
};
|
|
793
|
+
process.on("SIGINT", shutdown);
|
|
794
|
+
process.on("SIGTERM", shutdown);
|
|
795
|
+
}).addCommand(listCommand2).addCommand(templateCommand);
|
|
796
|
+
|
|
797
|
+
// src/commands/replay.ts
|
|
798
|
+
var import_commander3 = require("commander");
|
|
799
|
+
var import_prompts2 = __toESM(require("prompts"), 1);
|
|
800
|
+
var import_ora2 = __toESM(require("ora"), 1);
|
|
801
|
+
var replay = new import_commander3.Command().name("replay").argument(
|
|
802
|
+
"[captureId]",
|
|
803
|
+
"ID of the captured webhook to replay (optional - will prompt if not provided)"
|
|
804
|
+
).argument(
|
|
805
|
+
"[targetUrl]",
|
|
806
|
+
"Target URL to replay the webhook to (optional - will prompt if not provided)"
|
|
807
|
+
).description("Replay a captured webhook to a target URL").option("-m, --method <method>", "Override HTTP method").option(
|
|
808
|
+
"-H, --header <header>",
|
|
809
|
+
"Add custom header (format: key:value)",
|
|
810
|
+
(value, previous) => {
|
|
811
|
+
const [key, ...valueParts] = value.split(":");
|
|
812
|
+
const headerValue = valueParts.join(":");
|
|
813
|
+
if (!key || !headerValue) {
|
|
814
|
+
throw new Error("Header format should be key:value");
|
|
238
815
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
816
|
+
return (previous || []).concat([
|
|
817
|
+
{ key: key.trim(), value: headerValue.trim() }
|
|
818
|
+
]);
|
|
819
|
+
},
|
|
820
|
+
[]
|
|
821
|
+
).action(
|
|
822
|
+
async (captureId, targetUrl, options = {}) => {
|
|
823
|
+
const cwd = process.cwd();
|
|
824
|
+
const capturesDir = findCapturesDir(cwd);
|
|
825
|
+
const replayer = new WebhookReplayer(capturesDir);
|
|
826
|
+
const spinner = (0, import_ora2.default)("Loading captured webhooks...").start();
|
|
827
|
+
const captured = replayer.listCaptured();
|
|
828
|
+
spinner.stop();
|
|
829
|
+
if (captured.length === 0) {
|
|
830
|
+
console.log("\u{1F4ED} No captured webhooks found.");
|
|
831
|
+
console.log(
|
|
832
|
+
"\u{1F4A1} Run 'better-webhook capture' to start capturing webhooks first."
|
|
833
|
+
);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
let selectedCaptureId = captureId;
|
|
837
|
+
let selectedTargetUrl = targetUrl;
|
|
838
|
+
if (!selectedCaptureId) {
|
|
839
|
+
const choices = captured.map((c) => {
|
|
840
|
+
const date = new Date(c.capture.timestamp).toLocaleString();
|
|
841
|
+
const bodySize = c.capture.rawBody?.length ?? 0;
|
|
842
|
+
return {
|
|
843
|
+
title: `${c.capture.id} - ${c.capture.method} ${c.capture.url}`,
|
|
844
|
+
description: `${date} | Body: ${bodySize} bytes`,
|
|
845
|
+
value: c.capture.id
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
const response = await (0, import_prompts2.default)({
|
|
849
|
+
type: "select",
|
|
850
|
+
name: "captureId",
|
|
851
|
+
message: "Select a captured webhook to replay:",
|
|
852
|
+
choices,
|
|
853
|
+
initial: 0
|
|
854
|
+
});
|
|
855
|
+
if (!response.captureId) {
|
|
856
|
+
console.log("\u274C No webhook selected. Exiting.");
|
|
857
|
+
process.exitCode = 1;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
selectedCaptureId = response.captureId;
|
|
861
|
+
}
|
|
862
|
+
if (!selectedTargetUrl) {
|
|
863
|
+
const response = await (0, import_prompts2.default)({
|
|
864
|
+
type: "text",
|
|
865
|
+
name: "targetUrl",
|
|
866
|
+
message: "Enter the target URL to replay to:",
|
|
867
|
+
initial: "http://localhost:3000/webhook",
|
|
868
|
+
validate: (value) => {
|
|
869
|
+
try {
|
|
870
|
+
new URL(value);
|
|
871
|
+
return true;
|
|
872
|
+
} catch {
|
|
873
|
+
return "Please enter a valid URL";
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
if (!response.targetUrl) {
|
|
878
|
+
console.log("\u274C No target URL provided. Exiting.");
|
|
879
|
+
process.exitCode = 1;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
selectedTargetUrl = response.targetUrl;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const result = await replayer.replay(
|
|
886
|
+
selectedCaptureId,
|
|
887
|
+
selectedTargetUrl,
|
|
888
|
+
{
|
|
889
|
+
method: options.method,
|
|
890
|
+
headers: options.header
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
console.log("\u2705 Replay completed successfully!");
|
|
894
|
+
console.log("Status:", result.status);
|
|
895
|
+
console.log("Headers:");
|
|
896
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
897
|
+
console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
|
|
898
|
+
}
|
|
899
|
+
if (result.json !== void 0) {
|
|
900
|
+
console.log("Response JSON:");
|
|
901
|
+
console.log(JSON.stringify(result.json, null, 2));
|
|
902
|
+
} else {
|
|
903
|
+
console.log("Response Body:");
|
|
904
|
+
console.log(result.bodyText);
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error("\u274C Replay failed:", error.message);
|
|
908
|
+
process.exitCode = 1;
|
|
245
909
|
}
|
|
246
|
-
} catch (err) {
|
|
247
|
-
console.error("Request failed:", err.message);
|
|
248
|
-
process.exitCode = 1;
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
function statExists(p) {
|
|
252
|
-
try {
|
|
253
|
-
(0, import_fs2.statSync)(p);
|
|
254
|
-
return true;
|
|
255
|
-
} catch {
|
|
256
|
-
return false;
|
|
257
910
|
}
|
|
258
|
-
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
// src/index.ts
|
|
914
|
+
var program = new import_commander4.Command().name("better-webhook").description("CLI for listing, downloading and executing predefined webhooks").version("0.2.0");
|
|
915
|
+
program.addCommand(webhooks).addCommand(capture).addCommand(replay);
|
|
259
916
|
program.parseAsync(process.argv);
|