@better-webhook/cli 0.2.0 → 0.3.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/dist/index.cjs CHANGED
@@ -2,12 +2,38 @@
2
2
  "use strict";
3
3
 
4
4
  // src/index.ts
5
+ var import_commander4 = require("commander");
6
+
7
+ // src/commands/webhooks.ts
5
8
  var import_commander = require("commander");
6
- var import_fs2 = require("fs");
9
+ var import_path2 = require("path");
10
+ var import_fs3 = require("fs");
11
+
12
+ // src/utils/index.ts
13
+ var import_fs = require("fs");
7
14
  var import_path = require("path");
15
+ function findWebhooksDir(cwd) {
16
+ return (0, import_path.resolve)(cwd, ".webhooks");
17
+ }
18
+ function listWebhookFiles(dir) {
19
+ return listJsonFiles(dir);
20
+ }
21
+ function findCapturesDir(cwd) {
22
+ return (0, import_path.resolve)(cwd, ".webhook-captures");
23
+ }
24
+ function listJsonFiles(dir) {
25
+ try {
26
+ const entries = (0, import_fs.readdirSync)(dir);
27
+ return entries.filter(
28
+ (e) => (0, import_fs.statSync)((0, import_path.join)(dir, e)).isFile() && (0, import_path.extname)(e) === ".json"
29
+ );
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
8
34
 
9
35
  // src/loader.ts
10
- var import_fs = require("fs");
36
+ var import_fs2 = require("fs");
11
37
 
12
38
  // src/schema.ts
13
39
  var import_zod = require("zod");
@@ -47,7 +73,7 @@ ${issues}`);
47
73
  function loadWebhookFile(path) {
48
74
  let rawContent;
49
75
  try {
50
- rawContent = (0, import_fs.readFileSync)(path, "utf8");
76
+ rawContent = (0, import_fs2.readFileSync)(path, "utf8");
51
77
  } catch (e) {
52
78
  throw new Error(`Failed to read file ${path}: ${e.message}`);
53
79
  }
@@ -96,28 +122,85 @@ async function executeWebhook(def) {
96
122
  };
97
123
  }
98
124
 
99
- // src/index.ts
125
+ // src/commands/webhooks.ts
126
+ var import_fs4 = require("fs");
100
127
  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");
103
- function findWebhooksDir(cwd) {
104
- return (0, import_path.resolve)(cwd, ".webhooks");
105
- }
106
- function listWebhookFiles(dir) {
107
- try {
108
- const entries = (0, import_fs2.readdirSync)(dir);
109
- return entries.filter(
110
- (e) => (0, import_fs2.statSync)((0, import_path.join)(dir, e)).isFile() && (0, import_path.extname)(e) === ".json"
111
- );
112
- } catch {
113
- return [];
114
- }
115
- }
116
128
  var TEMPLATE_REPO_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
117
129
  var TEMPLATES = {
118
130
  "stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
119
131
  };
120
- program.command("download [name]").description(
132
+ function statExists(p) {
133
+ try {
134
+ (0, import_fs3.statSync)(p);
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ var listCommand = new import_commander.Command().name("list").description("List available webhook JSON definitions in .webhooks directory").action(() => {
141
+ const cwd = process.cwd();
142
+ const dir = findWebhooksDir(cwd);
143
+ const files = listWebhookFiles(dir);
144
+ if (!files.length) {
145
+ console.log("No webhook definitions found in .webhooks");
146
+ return;
147
+ }
148
+ files.forEach((f) => console.log((0, import_path2.basename)(f, ".json")));
149
+ });
150
+ var runCommand = new import_commander.Command().name("run").argument("<nameOrPath>", "Webhook name (in .webhooks) or path to JSON file").description(
151
+ "Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
152
+ ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options) => {
153
+ const cwd = process.cwd();
154
+ let filePath;
155
+ if (nameOrPath.endsWith(".json") && !nameOrPath.includes("/") && !nameOrPath.startsWith(".")) {
156
+ filePath = (0, import_path2.join)(findWebhooksDir(cwd), nameOrPath);
157
+ } else {
158
+ const candidate = (0, import_path2.join)(
159
+ findWebhooksDir(cwd),
160
+ nameOrPath + (nameOrPath.endsWith(".json") ? "" : ".json")
161
+ );
162
+ if (statExists(candidate)) {
163
+ filePath = candidate;
164
+ } else {
165
+ filePath = (0, import_path2.resolve)(cwd, nameOrPath);
166
+ }
167
+ }
168
+ if (!statExists(filePath)) {
169
+ console.error(`Webhook file not found: ${filePath}`);
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ let def;
174
+ try {
175
+ def = loadWebhookFile(filePath);
176
+ } catch (err) {
177
+ console.error(err.message);
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+ if (options.url) def = { ...def, url: options.url };
182
+ if (options.method)
183
+ def = { ...def, method: options.method.toUpperCase() };
184
+ try {
185
+ const result = await executeWebhook(def);
186
+ console.log("Status:", result.status);
187
+ console.log("Headers:");
188
+ for (const [k, v] of Object.entries(result.headers)) {
189
+ console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
190
+ }
191
+ if (result.json !== void 0) {
192
+ console.log("Response JSON:");
193
+ console.log(JSON.stringify(result.json, null, 2));
194
+ } else {
195
+ console.log("Response Body:");
196
+ console.log(result.bodyText);
197
+ }
198
+ } catch (err) {
199
+ console.error("Request failed:", err.message);
200
+ process.exitCode = 1;
201
+ }
202
+ });
203
+ var downloadCommand = new import_commander.Command().name("download").argument("[name]", "Template name to download").description(
121
204
  "Download official webhook template(s) into the .webhooks directory. If no name is provided, prints available templates."
122
205
  ).option("-a, --all", "Download all available templates").option("-f, --force", "Overwrite existing files if they exist").action(
123
206
  async (name, opts) => {
@@ -128,12 +211,12 @@ program.command("download [name]").description(
128
211
  }
129
212
  const cwd = process.cwd();
130
213
  const dir = findWebhooksDir(cwd);
131
- (0, import_fs2.mkdirSync)(dir, { recursive: true });
214
+ (0, import_fs4.mkdirSync)(dir, { recursive: true });
132
215
  const toDownload = opts.all ? Object.keys(TEMPLATES) : name ? [name] : [];
133
216
  if (!toDownload.length) {
134
217
  console.log("Available templates:");
135
218
  for (const key of Object.keys(TEMPLATES)) console.log(` - ${key}`);
136
- console.log("Use: better-webhook download <name> OR --all");
219
+ console.log("Use: better-webhook webhooks download <name> OR --all");
137
220
  return;
138
221
  }
139
222
  for (const templateName of toDownload) {
@@ -169,15 +252,15 @@ program.command("download [name]").description(
169
252
  console.error(`Template failed schema validation: ${e.message}`);
170
253
  continue;
171
254
  }
172
- const fileName = (0, import_path.basename)(rel);
173
- const destPath = (0, import_path.join)(dir, fileName);
174
- if ((0, import_fs2.existsSync)(destPath) && !opts.force) {
255
+ const fileName = (0, import_path2.basename)(rel);
256
+ const destPath = (0, import_path2.join)(dir, fileName);
257
+ if ((0, import_fs4.existsSync)(destPath) && !opts.force) {
175
258
  console.log(
176
259
  `Skipping existing file ${fileName} (use --force to overwrite)`
177
260
  );
178
261
  continue;
179
262
  }
180
- (0, import_fs2.writeFileSync)(destPath, JSON.stringify(json, null, 2));
263
+ (0, import_fs4.writeFileSync)(destPath, JSON.stringify(json, null, 2));
181
264
  console.log(`Downloaded ${templateName} -> .webhooks/${fileName}`);
182
265
  } catch (e) {
183
266
  console.error(`Error downloading ${templateName}: ${e.message}`);
@@ -185,75 +268,449 @@ program.command("download [name]").description(
185
268
  }
186
269
  }
187
270
  );
188
- program.command("list").description("List available webhook JSON definitions in .webhooks directory").action(() => {
189
- const cwd = process.cwd();
190
- const dir = findWebhooksDir(cwd);
191
- const files = listWebhookFiles(dir);
192
- if (!files.length) {
193
- console.log("No webhook definitions found in .webhooks");
194
- return;
271
+ var webhooks = new import_commander.Command().name("webhooks").description("Manage and execute webhook definitions").addCommand(listCommand).addCommand(runCommand).addCommand(downloadCommand);
272
+
273
+ // src/commands/capture.ts
274
+ var import_commander2 = require("commander");
275
+ var import_fs7 = require("fs");
276
+ var import_path5 = require("path");
277
+
278
+ // src/capture.ts
279
+ var import_http2 = require("http");
280
+ var import_fs5 = require("fs");
281
+ var import_path3 = require("path");
282
+ var import_crypto = require("crypto");
283
+ var WebhookCaptureServer = class {
284
+ server = null;
285
+ capturesDir;
286
+ constructor(capturesDir) {
287
+ this.capturesDir = capturesDir;
288
+ if (!(0, import_fs5.existsSync)(capturesDir)) {
289
+ (0, import_fs5.mkdirSync)(capturesDir, { recursive: true });
290
+ }
195
291
  }
196
- files.forEach((f) => console.log((0, import_path.basename)(f, ".json")));
197
- });
198
- program.command("run <nameOrPath>").description(
199
- "Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
200
- ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options) => {
201
- const cwd = process.cwd();
202
- let filePath;
203
- if (nameOrPath.endsWith(".json") && !nameOrPath.includes("/") && !nameOrPath.startsWith(".")) {
204
- filePath = (0, import_path.join)(findWebhooksDir(cwd), nameOrPath);
205
- } else {
206
- const candidate = (0, import_path.join)(
207
- findWebhooksDir(cwd),
208
- nameOrPath + (nameOrPath.endsWith(".json") ? "" : ".json")
292
+ start(startPort = 3001, maxAttempts = 20) {
293
+ return new Promise((resolve4, reject) => {
294
+ if (!Number.isInteger(startPort) || startPort < 0 || startPort > 65535) {
295
+ startPort = 3001;
296
+ }
297
+ let attempt = 0;
298
+ const tryListen = (portToTry) => {
299
+ this.server = (0, import_http2.createServer)(
300
+ async (req, res) => {
301
+ try {
302
+ await this.handleRequest(req, res);
303
+ } catch (error) {
304
+ console.error("Error handling request:", error);
305
+ res.statusCode = 500;
306
+ res.end("Internal Server Error");
307
+ }
308
+ }
309
+ );
310
+ const onError = (err) => {
311
+ this.server?.off("error", onError);
312
+ this.server?.off("listening", onListening);
313
+ if (err && err.code === "EADDRINUSE") {
314
+ attempt += 1;
315
+ if (startPort === 0) {
316
+ reject(new Error("Failed to bind to an ephemeral port."));
317
+ return;
318
+ }
319
+ if (attempt >= maxAttempts) {
320
+ reject(
321
+ new Error(
322
+ `All ${maxAttempts} port attempts starting at ${startPort} are in use.`
323
+ )
324
+ );
325
+ return;
326
+ }
327
+ const nextPort = startPort + attempt;
328
+ tryListen(nextPort);
329
+ } else {
330
+ reject(err);
331
+ }
332
+ };
333
+ const onListening = () => {
334
+ this.server?.off("error", onError);
335
+ this.server?.off("listening", onListening);
336
+ const address = this.server?.address();
337
+ const actualPort = address?.port ?? portToTry;
338
+ console.log(
339
+ `\u{1F3A3} Webhook capture server running on http://localhost:${actualPort}`
340
+ );
341
+ console.log(
342
+ `\u{1F4C1} Captured webhooks will be saved to: ${this.capturesDir}`
343
+ );
344
+ console.log(
345
+ "\u{1F4A1} Send webhooks to any path on this server to capture them"
346
+ );
347
+ console.log("\u23F9\uFE0F Press Ctrl+C to stop the server");
348
+ resolve4(actualPort);
349
+ };
350
+ this.server.on("error", onError);
351
+ this.server.on("listening", onListening);
352
+ this.server.listen(portToTry);
353
+ };
354
+ tryListen(startPort === 0 ? 0 : startPort);
355
+ });
356
+ }
357
+ stop() {
358
+ return new Promise((resolve4) => {
359
+ if (this.server) {
360
+ this.server.close(() => {
361
+ console.log("\u{1F4F4} Webhook capture server stopped");
362
+ resolve4();
363
+ });
364
+ } else {
365
+ resolve4();
366
+ }
367
+ });
368
+ }
369
+ async handleRequest(req, res) {
370
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
371
+ const id = this.generateId();
372
+ const url = req.url || "/";
373
+ const urlParts = new URL(url, `http://${req.headers.host || "localhost"}`);
374
+ const query = {};
375
+ for (const [key, value] of urlParts.searchParams.entries()) {
376
+ if (query[key]) {
377
+ if (Array.isArray(query[key])) {
378
+ query[key].push(value);
379
+ } else {
380
+ query[key] = [query[key], value];
381
+ }
382
+ } else {
383
+ query[key] = value;
384
+ }
385
+ }
386
+ const chunks = [];
387
+ req.on("data", (chunk) => chunks.push(chunk));
388
+ await new Promise((resolve4) => {
389
+ req.on("end", () => resolve4());
390
+ });
391
+ const rawBody = Buffer.concat(chunks).toString("utf8");
392
+ let parsedBody;
393
+ try {
394
+ parsedBody = rawBody ? JSON.parse(rawBody) : null;
395
+ } catch {
396
+ parsedBody = rawBody || null;
397
+ }
398
+ const captured = {
399
+ id,
400
+ timestamp,
401
+ method: req.method || "GET",
402
+ url: urlParts.pathname,
403
+ headers: req.headers,
404
+ body: parsedBody,
405
+ rawBody,
406
+ query
407
+ };
408
+ const filename = `${timestamp.replace(/[:.]/g, "-")}_${id}.json`;
409
+ const filepath = (0, import_path3.join)(this.capturesDir, filename);
410
+ try {
411
+ (0, import_fs5.writeFileSync)(filepath, JSON.stringify(captured, null, 2));
412
+ console.log(
413
+ `\u{1F4E6} Captured ${req.method} ${urlParts.pathname} -> ${filename}`
414
+ );
415
+ } catch (error) {
416
+ console.error(`\u274C Failed to save capture: ${error}`);
417
+ }
418
+ res.statusCode = 200;
419
+ res.setHeader("Content-Type", "application/json");
420
+ res.end(
421
+ JSON.stringify(
422
+ {
423
+ message: "Webhook captured successfully",
424
+ id,
425
+ timestamp,
426
+ file: filename
427
+ },
428
+ null,
429
+ 2
430
+ )
209
431
  );
210
- if (statExists(candidate)) {
211
- filePath = candidate;
432
+ }
433
+ generateId() {
434
+ try {
435
+ return (0, import_crypto.randomUUID)();
436
+ } catch {
437
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
438
+ }
439
+ }
440
+ };
441
+
442
+ // src/replay.ts
443
+ var import_fs6 = require("fs");
444
+ var import_path4 = require("path");
445
+ var WebhookReplayer = class {
446
+ constructor(capturesDir) {
447
+ this.capturesDir = capturesDir;
448
+ }
449
+ /**
450
+ * List all captured webhooks
451
+ */
452
+ listCaptured() {
453
+ try {
454
+ const files = listJsonFiles(this.capturesDir).sort().reverse();
455
+ return files.map((file) => ({
456
+ file,
457
+ capture: this.loadCapture((0, import_path4.join)(this.capturesDir, file))
458
+ }));
459
+ } catch {
460
+ return [];
461
+ }
462
+ }
463
+ /**
464
+ * Load a specific captured webhook by filename or ID
465
+ */
466
+ loadCapture(filePathOrId) {
467
+ let filepath;
468
+ if (filePathOrId.includes("/") || filePathOrId.endsWith(".json")) {
469
+ filepath = filePathOrId;
212
470
  } else {
213
- filePath = (0, import_path.resolve)(cwd, nameOrPath);
471
+ const files = listJsonFiles(this.capturesDir).filter(
472
+ (f) => f.includes(filePathOrId)
473
+ );
474
+ if (files.length === 0) {
475
+ throw new Error(`No capture found with ID: ${filePathOrId}`);
476
+ }
477
+ if (files.length > 1) {
478
+ throw new Error(
479
+ `Multiple captures found with ID ${filePathOrId}: ${files.join(", ")}`
480
+ );
481
+ }
482
+ filepath = (0, import_path4.join)(this.capturesDir, files[0] ?? "");
483
+ }
484
+ try {
485
+ const content = (0, import_fs6.readFileSync)(filepath, "utf8");
486
+ return JSON.parse(content);
487
+ } catch (error) {
488
+ throw new Error(
489
+ `Failed to load capture from ${filepath}: ${error.message}`
490
+ );
214
491
  }
215
492
  }
216
- if (!statExists(filePath)) {
217
- console.error(`Webhook file not found: ${filePath}`);
493
+ /**
494
+ * Replay a captured webhook to a target URL
495
+ */
496
+ async replay(captureId, targetUrl, options = {}) {
497
+ const capture2 = this.loadCapture(captureId);
498
+ const webhookDef = {
499
+ url: options.url || targetUrl,
500
+ method: options.method || capture2.method,
501
+ headers: options.headers || this.convertHeaders(capture2.headers),
502
+ body: capture2.body
503
+ };
504
+ console.log(`\u{1F504} Replaying webhook ${capture2.id} (${capture2.timestamp})`);
505
+ console.log(` Method: ${webhookDef.method}`);
506
+ console.log(` URL: ${webhookDef.url}`);
507
+ console.log(` Original: ${capture2.method} ${capture2.url}`);
508
+ try {
509
+ const result = await executeWebhook(webhookDef);
510
+ return result;
511
+ } catch (error) {
512
+ throw new Error(`Replay failed: ${error.message}`);
513
+ }
514
+ }
515
+ /**
516
+ * Convert captured webhook to template format
517
+ */
518
+ captureToTemplate(captureId, templateUrl) {
519
+ const capture2 = this.loadCapture(captureId);
520
+ return {
521
+ url: templateUrl || "http://localhost:3000/webhook",
522
+ method: capture2.method,
523
+ headers: this.convertHeaders(capture2.headers),
524
+ body: capture2.body
525
+ };
526
+ }
527
+ /**
528
+ * Convert captured headers to template format
529
+ */
530
+ convertHeaders(headers) {
531
+ const result = [];
532
+ for (const [key, value] of Object.entries(headers)) {
533
+ const skipHeaders = [
534
+ "host",
535
+ "content-length",
536
+ "connection",
537
+ "accept-encoding",
538
+ "user-agent",
539
+ "x-forwarded-for",
540
+ "x-forwarded-proto"
541
+ ];
542
+ if (skipHeaders.includes(key.toLowerCase())) {
543
+ continue;
544
+ }
545
+ if (Array.isArray(value)) {
546
+ if (value.length === 1) {
547
+ result.push({ key, value: value[0] ?? "" });
548
+ } else {
549
+ result.push({ key, value: value.join(", ") });
550
+ }
551
+ } else {
552
+ result.push({ key, value });
553
+ }
554
+ }
555
+ return result;
556
+ }
557
+ /**
558
+ * Get summary information about a capture
559
+ */
560
+ getCaptureSummary(captureId) {
561
+ const capture2 = this.loadCapture(captureId);
562
+ const date = new Date(capture2.timestamp);
563
+ const bodySize = capture2.rawBody ? capture2.rawBody.length : 0;
564
+ const headerCount = Object.keys(capture2.headers).length;
565
+ return [
566
+ `ID: ${capture2.id}`,
567
+ `Date: ${date.toLocaleString()}`,
568
+ `Method: ${capture2.method}`,
569
+ `Path: ${capture2.url}`,
570
+ `Headers: ${headerCount}`,
571
+ `Body Size: ${bodySize} bytes`,
572
+ `Query Params: ${Object.keys(capture2.query).length}`
573
+ ].join("\n");
574
+ }
575
+ };
576
+
577
+ // src/commands/capture.ts
578
+ 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) => {
579
+ const cwd = process.cwd();
580
+ const capturesDir = findCapturesDir(cwd);
581
+ const replayer = new WebhookReplayer(capturesDir);
582
+ const captures = replayer.listCaptured();
583
+ const limit = parseInt(options.limit, 10);
584
+ if (Number.isNaN(limit) || limit <= 0) {
585
+ console.error("Invalid --limit: must be a positive integer.");
218
586
  process.exitCode = 1;
219
587
  return;
220
588
  }
221
- let def;
589
+ if (captures.length === 0) {
590
+ console.log("No webhook captures found.");
591
+ console.log(`Run 'better-webhook capture' to start capturing webhooks.`);
592
+ return;
593
+ }
594
+ console.log(
595
+ `\u{1F4CB} Found ${captures.length} captured webhooks (showing ${Math.min(limit, captures.length)}):
596
+ `
597
+ );
598
+ captures.slice(0, limit).forEach(({ file, capture: capture2 }) => {
599
+ const date = new Date(capture2.timestamp).toLocaleString();
600
+ const bodySize = capture2.rawBody ? capture2.rawBody.length : 0;
601
+ console.log(`\u{1F194} ${capture2.id} | \u{1F4C5} ${date}`);
602
+ console.log(` ${capture2.method} ${capture2.url} | ${bodySize} bytes`);
603
+ console.log(` \u{1F4C4} ${file}
604
+ `);
605
+ });
606
+ if (captures.length > limit) {
607
+ console.log(
608
+ `... and ${captures.length - limit} more. Use --limit to show more.`
609
+ );
610
+ }
611
+ });
612
+ 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(
613
+ "-u, --url <url>",
614
+ "Template URL (default: http://localhost:3000/webhook)"
615
+ ).option("-o, --output-dir <dir>", "Output directory (default: .webhooks)").action(
616
+ async (captureId, templateName, options) => {
617
+ const cwd = process.cwd();
618
+ const capturesDir = findCapturesDir(cwd);
619
+ const replayer = new WebhookReplayer(capturesDir);
620
+ try {
621
+ const template = replayer.captureToTemplate(captureId, options?.url);
622
+ if (!templateName) {
623
+ const capture2 = replayer.loadCapture(captureId);
624
+ const date = new Date(capture2.timestamp).toISOString().split("T")[0];
625
+ const pathPart = capture2.url.replace(/[^a-zA-Z0-9]/g, "_").substring(1) || "webhook";
626
+ templateName = `captured_${date}_${pathPart}_${capture2.id}`;
627
+ }
628
+ const outputDir = options?.outputDir ? (0, import_path5.resolve)(cwd, options.outputDir) : findWebhooksDir(cwd);
629
+ (0, import_fs7.mkdirSync)(outputDir, { recursive: true });
630
+ const templatePath = (0, import_path5.join)(outputDir, `${templateName}.json`);
631
+ (0, import_fs7.writeFileSync)(templatePath, JSON.stringify(template, null, 2));
632
+ console.log(`\u2705 Template created: ${templatePath}`);
633
+ console.log(
634
+ `\u{1F504} Run it with: better-webhook webhooks run ${templateName}`
635
+ );
636
+ console.log("\n\u{1F4CA} Template Summary:");
637
+ console.log(replayer.getCaptureSummary(captureId));
638
+ } catch (error) {
639
+ console.error("\u274C Template generation failed:", error.message);
640
+ process.exitCode = 1;
641
+ }
642
+ }
643
+ );
644
+ var capture = new import_commander2.Command().name("capture").description(
645
+ "Start a server to capture incoming webhook requests, or list captured webhooks"
646
+ ).option("-p, --port <port>", "Port to listen on", "3001").action(async (options) => {
647
+ const cwd = process.cwd();
648
+ const capturesDir = findCapturesDir(cwd);
649
+ const server = new WebhookCaptureServer(capturesDir);
650
+ let actualPort;
222
651
  try {
223
- def = loadWebhookFile(filePath);
224
- } catch (err) {
225
- console.error(err.message);
652
+ actualPort = await server.start(parseInt(options.port));
653
+ } catch (error) {
654
+ console.error("Failed to start capture server:", error.message);
226
655
  process.exitCode = 1;
227
656
  return;
228
657
  }
229
- if (options.url) def = { ...def, url: options.url };
230
- if (options.method)
231
- def = { ...def, method: options.method.toUpperCase() };
232
- try {
233
- const result = await executeWebhook(def);
234
- console.log("Status:", result.status);
235
- console.log("Headers:");
236
- for (const [k, v] of Object.entries(result.headers)) {
237
- console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
658
+ const shutdown = async () => {
659
+ console.log("\n\u{1F6D1} Shutting down server...");
660
+ await server.stop();
661
+ process.exit(0);
662
+ };
663
+ process.on("SIGINT", shutdown);
664
+ process.on("SIGTERM", shutdown);
665
+ }).addCommand(listCommand2).addCommand(templateCommand);
666
+
667
+ // src/commands/replay.ts
668
+ var import_commander3 = require("commander");
669
+ var replay = new import_commander3.Command().name("replay").argument("<captureId>", "ID of the captured webhook to replay").argument("<targetUrl>", "Target URL to replay the webhook to").description("Replay a captured webhook to a target URL").option("-m, --method <method>", "Override HTTP method").option(
670
+ "-H, --header <header>",
671
+ "Add custom header (format: key:value)",
672
+ (value, previous) => {
673
+ const [key, ...valueParts] = value.split(":");
674
+ const headerValue = valueParts.join(":");
675
+ if (!key || !headerValue) {
676
+ throw new Error("Header format should be key:value");
238
677
  }
239
- if (result.json !== void 0) {
240
- console.log("Response JSON:");
241
- console.log(JSON.stringify(result.json, null, 2));
242
- } else {
243
- console.log("Response Body:");
244
- console.log(result.bodyText);
678
+ return (previous || []).concat([
679
+ { key: key.trim(), value: headerValue.trim() }
680
+ ]);
681
+ },
682
+ []
683
+ ).action(
684
+ async (captureId, targetUrl, options) => {
685
+ const cwd = process.cwd();
686
+ const capturesDir = findCapturesDir(cwd);
687
+ const replayer = new WebhookReplayer(capturesDir);
688
+ try {
689
+ const result = await replayer.replay(captureId, targetUrl, {
690
+ method: options.method,
691
+ headers: options.header
692
+ });
693
+ console.log("\u2705 Replay completed successfully!");
694
+ console.log("Status:", result.status);
695
+ console.log("Headers:");
696
+ for (const [k, v] of Object.entries(result.headers)) {
697
+ console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
698
+ }
699
+ if (result.json !== void 0) {
700
+ console.log("Response JSON:");
701
+ console.log(JSON.stringify(result.json, null, 2));
702
+ } else {
703
+ console.log("Response Body:");
704
+ console.log(result.bodyText);
705
+ }
706
+ } catch (error) {
707
+ console.error("\u274C Replay failed:", error.message);
708
+ process.exitCode = 1;
245
709
  }
246
- } catch (err) {
247
- console.error("Request failed:", err.message);
248
- process.exitCode = 1;
249
710
  }
250
- });
251
- function statExists(p) {
252
- try {
253
- (0, import_fs2.statSync)(p);
254
- return true;
255
- } catch {
256
- return false;
257
- }
258
- }
711
+ );
712
+
713
+ // src/index.ts
714
+ var program = new import_commander4.Command().name("better-webhook").description("CLI for listing, downloading and executing predefined webhooks").version("0.2.0");
715
+ program.addCommand(webhooks).addCommand(capture).addCommand(replay);
259
716
  program.parseAsync(process.argv);