@better-webhook/cli 0.3.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.
Files changed (3) hide show
  1. package/dist/index.cjs +271 -71
  2. package/dist/index.js +235 -57
  3. package/package.json +7 -3
package/dist/index.cjs CHANGED
@@ -1,5 +1,27 @@
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
5
27
  var import_commander4 = require("commander");
@@ -7,33 +29,11 @@ var import_commander4 = require("commander");
7
29
  // src/commands/webhooks.ts
8
30
  var import_commander = require("commander");
9
31
  var import_path2 = require("path");
10
- var import_fs3 = require("fs");
32
+ var import_fs2 = require("fs");
11
33
 
12
34
  // src/utils/index.ts
13
35
  var import_fs = require("fs");
14
36
  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
- }
34
-
35
- // src/loader.ts
36
- var import_fs2 = require("fs");
37
37
 
38
38
  // src/schema.ts
39
39
  var import_zod = require("zod");
@@ -69,24 +69,7 @@ ${issues}`);
69
69
  return parsed.data;
70
70
  }
71
71
 
72
- // src/loader.ts
73
- function loadWebhookFile(path) {
74
- let rawContent;
75
- try {
76
- rawContent = (0, import_fs2.readFileSync)(path, "utf8");
77
- } catch (e) {
78
- throw new Error(`Failed to read file ${path}: ${e.message}`);
79
- }
80
- let json;
81
- try {
82
- json = JSON.parse(rawContent);
83
- } catch (e) {
84
- throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
85
- }
86
- return validateWebhookJSON(json, path);
87
- }
88
-
89
- // src/http.ts
72
+ // src/utils/http.ts
90
73
  var import_undici = require("undici");
91
74
  async function executeWebhook(def) {
92
75
  const headerMap = {};
@@ -122,16 +105,58 @@ async function executeWebhook(def) {
122
105
  };
123
106
  }
124
107
 
108
+ // src/utils/index.ts
109
+ function findWebhooksDir(cwd) {
110
+ return (0, import_path.resolve)(cwd, ".webhooks");
111
+ }
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) {
119
+ try {
120
+ const entries = (0, import_fs.readdirSync)(dir);
121
+ return entries.filter(
122
+ (e) => (0, import_fs.statSync)((0, import_path.join)(dir, e)).isFile() && (0, import_path.extname)(e) === ".json"
123
+ );
124
+ } catch {
125
+ return [];
126
+ }
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
+
125
144
  // src/commands/webhooks.ts
126
- var import_fs4 = require("fs");
145
+ var import_fs3 = require("fs");
127
146
  var import_undici2 = require("undici");
147
+
148
+ // src/config.ts
128
149
  var TEMPLATE_REPO_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
129
150
  var TEMPLATES = {
130
151
  "stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
131
152
  };
153
+
154
+ // src/commands/webhooks.ts
155
+ var import_prompts = __toESM(require("prompts"), 1);
156
+ var import_ora = __toESM(require("ora"), 1);
132
157
  function statExists(p) {
133
158
  try {
134
- (0, import_fs3.statSync)(p);
159
+ (0, import_fs2.statSync)(p);
135
160
  return true;
136
161
  } catch {
137
162
  return false;
@@ -142,27 +167,127 @@ var listCommand = new import_commander.Command().name("list").description("List
142
167
  const dir = findWebhooksDir(cwd);
143
168
  const files = listWebhookFiles(dir);
144
169
  if (!files.length) {
145
- console.log("No webhook definitions found in .webhooks");
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");
146
173
  return;
147
174
  }
148
- files.forEach((f) => console.log((0, import_path2.basename)(f, ".json")));
175
+ console.log("Available webhook definitions:");
176
+ files.forEach((f) => console.log(` \u2022 ${(0, import_path2.basename)(f, ".json")}`));
149
177
  });
150
- var runCommand = new import_commander.Command().name("run").argument("<nameOrPath>", "Webhook name (in .webhooks) or path to JSON file").description(
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(
151
182
  "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) => {
183
+ ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options = {}) => {
153
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
+ }
154
279
  let filePath;
155
- if (nameOrPath.endsWith(".json") && !nameOrPath.includes("/") && !nameOrPath.startsWith(".")) {
156
- filePath = (0, import_path2.join)(findWebhooksDir(cwd), nameOrPath);
280
+ if (selectedNameOrPath.endsWith(".json") && !selectedNameOrPath.includes("/") && !selectedNameOrPath.startsWith(".")) {
281
+ filePath = (0, import_path2.join)(webhooksDir, selectedNameOrPath);
157
282
  } else {
158
283
  const candidate = (0, import_path2.join)(
159
- findWebhooksDir(cwd),
160
- nameOrPath + (nameOrPath.endsWith(".json") ? "" : ".json")
284
+ webhooksDir,
285
+ selectedNameOrPath + (selectedNameOrPath.endsWith(".json") ? "" : ".json")
161
286
  );
162
287
  if (statExists(candidate)) {
163
288
  filePath = candidate;
164
289
  } else {
165
- filePath = (0, import_path2.resolve)(cwd, nameOrPath);
290
+ filePath = (0, import_path2.resolve)(cwd, selectedNameOrPath);
166
291
  }
167
292
  }
168
293
  if (!statExists(filePath)) {
@@ -181,8 +306,12 @@ var runCommand = new import_commander.Command().name("run").argument("<nameOrPat
181
306
  if (options.url) def = { ...def, url: options.url };
182
307
  if (options.method)
183
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();
184
312
  try {
185
313
  const result = await executeWebhook(def);
314
+ executeSpinner.succeed("Webhook executed successfully!");
186
315
  console.log("Status:", result.status);
187
316
  console.log("Headers:");
188
317
  for (const [k, v] of Object.entries(result.headers)) {
@@ -196,7 +325,8 @@ var runCommand = new import_commander.Command().name("run").argument("<nameOrPat
196
325
  console.log(result.bodyText);
197
326
  }
198
327
  } catch (err) {
199
- console.error("Request failed:", err.message);
328
+ executeSpinner.fail("Request failed");
329
+ console.error("Error:", err.message);
200
330
  process.exitCode = 1;
201
331
  }
202
332
  });
@@ -211,7 +341,7 @@ var downloadCommand = new import_commander.Command().name("download").argument("
211
341
  }
212
342
  const cwd = process.cwd();
213
343
  const dir = findWebhooksDir(cwd);
214
- (0, import_fs4.mkdirSync)(dir, { recursive: true });
344
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
215
345
  const toDownload = opts.all ? Object.keys(TEMPLATES) : name ? [name] : [];
216
346
  if (!toDownload.length) {
217
347
  console.log("Available templates:");
@@ -254,13 +384,13 @@ var downloadCommand = new import_commander.Command().name("download").argument("
254
384
  }
255
385
  const fileName = (0, import_path2.basename)(rel);
256
386
  const destPath = (0, import_path2.join)(dir, fileName);
257
- if ((0, import_fs4.existsSync)(destPath) && !opts.force) {
387
+ if ((0, import_fs3.existsSync)(destPath) && !opts.force) {
258
388
  console.log(
259
389
  `Skipping existing file ${fileName} (use --force to overwrite)`
260
390
  );
261
391
  continue;
262
392
  }
263
- (0, import_fs4.writeFileSync)(destPath, JSON.stringify(json, null, 2));
393
+ (0, import_fs3.writeFileSync)(destPath, JSON.stringify(json, null, 2));
264
394
  console.log(`Downloaded ${templateName} -> .webhooks/${fileName}`);
265
395
  } catch (e) {
266
396
  console.error(`Error downloading ${templateName}: ${e.message}`);
@@ -272,12 +402,12 @@ var webhooks = new import_commander.Command().name("webhooks").description("Mana
272
402
 
273
403
  // src/commands/capture.ts
274
404
  var import_commander2 = require("commander");
275
- var import_fs7 = require("fs");
405
+ var import_fs6 = require("fs");
276
406
  var import_path5 = require("path");
277
407
 
278
408
  // src/capture.ts
279
409
  var import_http2 = require("http");
280
- var import_fs5 = require("fs");
410
+ var import_fs4 = require("fs");
281
411
  var import_path3 = require("path");
282
412
  var import_crypto = require("crypto");
283
413
  var WebhookCaptureServer = class {
@@ -285,8 +415,8 @@ var WebhookCaptureServer = class {
285
415
  capturesDir;
286
416
  constructor(capturesDir) {
287
417
  this.capturesDir = capturesDir;
288
- if (!(0, import_fs5.existsSync)(capturesDir)) {
289
- (0, import_fs5.mkdirSync)(capturesDir, { recursive: true });
418
+ if (!(0, import_fs4.existsSync)(capturesDir)) {
419
+ (0, import_fs4.mkdirSync)(capturesDir, { recursive: true });
290
420
  }
291
421
  }
292
422
  start(startPort = 3001, maxAttempts = 20) {
@@ -408,7 +538,7 @@ var WebhookCaptureServer = class {
408
538
  const filename = `${timestamp.replace(/[:.]/g, "-")}_${id}.json`;
409
539
  const filepath = (0, import_path3.join)(this.capturesDir, filename);
410
540
  try {
411
- (0, import_fs5.writeFileSync)(filepath, JSON.stringify(captured, null, 2));
541
+ (0, import_fs4.writeFileSync)(filepath, JSON.stringify(captured, null, 2));
412
542
  console.log(
413
543
  `\u{1F4E6} Captured ${req.method} ${urlParts.pathname} -> ${filename}`
414
544
  );
@@ -440,7 +570,7 @@ var WebhookCaptureServer = class {
440
570
  };
441
571
 
442
572
  // src/replay.ts
443
- var import_fs6 = require("fs");
573
+ var import_fs5 = require("fs");
444
574
  var import_path4 = require("path");
445
575
  var WebhookReplayer = class {
446
576
  constructor(capturesDir) {
@@ -482,7 +612,7 @@ var WebhookReplayer = class {
482
612
  filepath = (0, import_path4.join)(this.capturesDir, files[0] ?? "");
483
613
  }
484
614
  try {
485
- const content = (0, import_fs6.readFileSync)(filepath, "utf8");
615
+ const content = (0, import_fs5.readFileSync)(filepath, "utf8");
486
616
  return JSON.parse(content);
487
617
  } catch (error) {
488
618
  throw new Error(
@@ -626,9 +756,9 @@ var templateCommand = new import_commander2.Command().name("template").argument(
626
756
  templateName = `captured_${date}_${pathPart}_${capture2.id}`;
627
757
  }
628
758
  const outputDir = options?.outputDir ? (0, import_path5.resolve)(cwd, options.outputDir) : findWebhooksDir(cwd);
629
- (0, import_fs7.mkdirSync)(outputDir, { recursive: true });
759
+ (0, import_fs6.mkdirSync)(outputDir, { recursive: true });
630
760
  const templatePath = (0, import_path5.join)(outputDir, `${templateName}.json`);
631
- (0, import_fs7.writeFileSync)(templatePath, JSON.stringify(template, null, 2));
761
+ (0, import_fs6.writeFileSync)(templatePath, JSON.stringify(template, null, 2));
632
762
  console.log(`\u2705 Template created: ${templatePath}`);
633
763
  console.log(
634
764
  `\u{1F504} Run it with: better-webhook webhooks run ${templateName}`
@@ -666,7 +796,15 @@ var capture = new import_commander2.Command().name("capture").description(
666
796
 
667
797
  // src/commands/replay.ts
668
798
  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(
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(
670
808
  "-H, --header <header>",
671
809
  "Add custom header (format: key:value)",
672
810
  (value, previous) => {
@@ -681,15 +819,77 @@ var replay = new import_commander3.Command().name("replay").argument("<captureId
681
819
  },
682
820
  []
683
821
  ).action(
684
- async (captureId, targetUrl, options) => {
822
+ async (captureId, targetUrl, options = {}) => {
685
823
  const cwd = process.cwd();
686
824
  const capturesDir = findCapturesDir(cwd);
687
825
  const replayer = new WebhookReplayer(capturesDir);
688
- try {
689
- const result = await replayer.replay(captureId, targetUrl, {
690
- method: options.method,
691
- headers: options.header
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
+ }
692
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
+ );
693
893
  console.log("\u2705 Replay completed successfully!");
694
894
  console.log("Status:", result.status);
695
895
  console.log("Headers:");
package/dist/index.js CHANGED
@@ -9,30 +9,8 @@ import { join as join2, resolve as resolve2, basename } from "path";
9
9
  import { statSync as statSync2 } from "fs";
10
10
 
11
11
  // src/utils/index.ts
12
- import { readdirSync, statSync } from "fs";
12
+ import { readdirSync, readFileSync, statSync } from "fs";
13
13
  import { join, resolve, extname } from "path";
14
- function findWebhooksDir(cwd) {
15
- return resolve(cwd, ".webhooks");
16
- }
17
- function listWebhookFiles(dir) {
18
- return listJsonFiles(dir);
19
- }
20
- function findCapturesDir(cwd) {
21
- return resolve(cwd, ".webhook-captures");
22
- }
23
- function listJsonFiles(dir) {
24
- try {
25
- const entries = readdirSync(dir);
26
- return entries.filter(
27
- (e) => statSync(join(dir, e)).isFile() && extname(e) === ".json"
28
- );
29
- } catch {
30
- return [];
31
- }
32
- }
33
-
34
- // src/loader.ts
35
- import { readFileSync } from "fs";
36
14
 
37
15
  // src/schema.ts
38
16
  import { z } from "zod";
@@ -68,24 +46,7 @@ ${issues}`);
68
46
  return parsed.data;
69
47
  }
70
48
 
71
- // src/loader.ts
72
- function loadWebhookFile(path) {
73
- let rawContent;
74
- try {
75
- rawContent = readFileSync(path, "utf8");
76
- } catch (e) {
77
- throw new Error(`Failed to read file ${path}: ${e.message}`);
78
- }
79
- let json;
80
- try {
81
- json = JSON.parse(rawContent);
82
- } catch (e) {
83
- throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
84
- }
85
- return validateWebhookJSON(json, path);
86
- }
87
-
88
- // src/http.ts
49
+ // src/utils/http.ts
89
50
  import { request } from "undici";
90
51
  async function executeWebhook(def) {
91
52
  const headerMap = {};
@@ -121,13 +82,55 @@ async function executeWebhook(def) {
121
82
  };
122
83
  }
123
84
 
85
+ // src/utils/index.ts
86
+ function findWebhooksDir(cwd) {
87
+ return resolve(cwd, ".webhooks");
88
+ }
89
+ function listWebhookFiles(dir) {
90
+ return listJsonFiles(dir);
91
+ }
92
+ function findCapturesDir(cwd) {
93
+ return resolve(cwd, ".webhook-captures");
94
+ }
95
+ function listJsonFiles(dir) {
96
+ try {
97
+ const entries = readdirSync(dir);
98
+ return entries.filter(
99
+ (e) => statSync(join(dir, e)).isFile() && extname(e) === ".json"
100
+ );
101
+ } catch {
102
+ return [];
103
+ }
104
+ }
105
+ function loadWebhookFile(path) {
106
+ let rawContent;
107
+ try {
108
+ rawContent = readFileSync(path, "utf8");
109
+ } catch (e) {
110
+ throw new Error(`Failed to read file ${path}: ${e.message}`);
111
+ }
112
+ let json;
113
+ try {
114
+ json = JSON.parse(rawContent);
115
+ } catch (e) {
116
+ throw new Error(`Invalid JSON in file ${path}: ${e.message}`);
117
+ }
118
+ return validateWebhookJSON(json, path);
119
+ }
120
+
124
121
  // src/commands/webhooks.ts
125
122
  import { mkdirSync, writeFileSync, existsSync } from "fs";
126
123
  import { request as request2 } from "undici";
124
+
125
+ // src/config.ts
127
126
  var TEMPLATE_REPO_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
128
127
  var TEMPLATES = {
129
128
  "stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
130
129
  };
130
+
131
+ // src/commands/webhooks.ts
132
+ import prompts from "prompts";
133
+ import ora from "ora";
131
134
  function statExists(p) {
132
135
  try {
133
136
  statSync2(p);
@@ -141,27 +144,127 @@ var listCommand = new Command().name("list").description("List available webhook
141
144
  const dir = findWebhooksDir(cwd);
142
145
  const files = listWebhookFiles(dir);
143
146
  if (!files.length) {
144
- console.log("No webhook definitions found in .webhooks");
147
+ console.log("\u{1F4ED} No webhook definitions found in .webhooks directory.");
148
+ console.log("\u{1F4A1} Create webhook files or download templates with:");
149
+ console.log(" better-webhook webhooks download --all");
145
150
  return;
146
151
  }
147
- files.forEach((f) => console.log(basename(f, ".json")));
152
+ console.log("Available webhook definitions:");
153
+ files.forEach((f) => console.log(` \u2022 ${basename(f, ".json")}`));
148
154
  });
149
- var runCommand = new Command().name("run").argument("<nameOrPath>", "Webhook name (in .webhooks) or path to JSON file").description(
155
+ var runCommand = new Command().name("run").argument(
156
+ "[nameOrPath]",
157
+ "Webhook name (in .webhooks) or path to JSON file (optional - will prompt if not provided)"
158
+ ).description(
150
159
  "Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
151
- ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options) => {
160
+ ).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options = {}) => {
152
161
  const cwd = process.cwd();
162
+ const webhooksDir = findWebhooksDir(cwd);
163
+ let selectedNameOrPath = nameOrPath;
164
+ if (!selectedNameOrPath) {
165
+ const spinner = ora("Loading available webhooks...").start();
166
+ const localFiles = listWebhookFiles(webhooksDir);
167
+ const templates = Object.keys(TEMPLATES);
168
+ spinner.stop();
169
+ if (localFiles.length === 0 && templates.length === 0) {
170
+ console.log("\u{1F4ED} No webhook definitions found.");
171
+ console.log(
172
+ "\u{1F4A1} Create webhook files in .webhooks directory or download templates with:"
173
+ );
174
+ console.log(" better-webhook webhooks download --all");
175
+ return;
176
+ }
177
+ const choices = [];
178
+ if (localFiles.length > 0) {
179
+ choices.push(
180
+ {
181
+ title: "--- Local Webhooks (.webhooks) ---",
182
+ description: "",
183
+ value: "",
184
+ type: "separator"
185
+ },
186
+ ...localFiles.map((file) => ({
187
+ title: basename(file, ".json"),
188
+ description: `Local file: ${file}`,
189
+ value: basename(file, ".json"),
190
+ type: "local"
191
+ }))
192
+ );
193
+ }
194
+ if (templates.length > 0) {
195
+ choices.push(
196
+ {
197
+ title: "--- Available Templates ---",
198
+ description: "",
199
+ value: "",
200
+ type: "separator"
201
+ },
202
+ ...templates.map((template) => ({
203
+ title: template,
204
+ description: `Template: ${TEMPLATES[template]}`,
205
+ value: template,
206
+ type: "template"
207
+ }))
208
+ );
209
+ }
210
+ const response = await prompts({
211
+ type: "select",
212
+ name: "webhook",
213
+ message: "Select a webhook to run:",
214
+ choices: choices.filter((choice) => choice.value !== ""),
215
+ // Remove separators for selection
216
+ initial: 0
217
+ });
218
+ if (!response.webhook) {
219
+ console.log("\u274C No webhook selected. Exiting.");
220
+ process.exitCode = 1;
221
+ return;
222
+ }
223
+ selectedNameOrPath = response.webhook;
224
+ if (selectedNameOrPath && templates.includes(selectedNameOrPath)) {
225
+ const downloadSpinner = ora(
226
+ `Downloading template: ${selectedNameOrPath}...`
227
+ ).start();
228
+ try {
229
+ const rel = TEMPLATES[selectedNameOrPath];
230
+ const rawUrl = `${TEMPLATE_REPO_BASE}/${rel}`;
231
+ const { statusCode, body } = await request2(rawUrl);
232
+ if (statusCode !== 200) {
233
+ throw new Error(`HTTP ${statusCode}`);
234
+ }
235
+ const text = await body.text();
236
+ const json = JSON.parse(text);
237
+ validateWebhookJSON(json, rawUrl);
238
+ mkdirSync(webhooksDir, { recursive: true });
239
+ const fileName = basename(rel);
240
+ const destPath = join2(webhooksDir, fileName);
241
+ writeFileSync(destPath, JSON.stringify(json, null, 2));
242
+ selectedNameOrPath = basename(fileName, ".json");
243
+ downloadSpinner.succeed(`Downloaded template: ${selectedNameOrPath}`);
244
+ } catch (error) {
245
+ downloadSpinner.fail(`Failed to download template: ${error.message}`);
246
+ process.exitCode = 1;
247
+ return;
248
+ }
249
+ }
250
+ }
251
+ if (!selectedNameOrPath) {
252
+ console.error("\u274C No webhook selected.");
253
+ process.exitCode = 1;
254
+ return;
255
+ }
153
256
  let filePath;
154
- if (nameOrPath.endsWith(".json") && !nameOrPath.includes("/") && !nameOrPath.startsWith(".")) {
155
- filePath = join2(findWebhooksDir(cwd), nameOrPath);
257
+ if (selectedNameOrPath.endsWith(".json") && !selectedNameOrPath.includes("/") && !selectedNameOrPath.startsWith(".")) {
258
+ filePath = join2(webhooksDir, selectedNameOrPath);
156
259
  } else {
157
260
  const candidate = join2(
158
- findWebhooksDir(cwd),
159
- nameOrPath + (nameOrPath.endsWith(".json") ? "" : ".json")
261
+ webhooksDir,
262
+ selectedNameOrPath + (selectedNameOrPath.endsWith(".json") ? "" : ".json")
160
263
  );
161
264
  if (statExists(candidate)) {
162
265
  filePath = candidate;
163
266
  } else {
164
- filePath = resolve2(cwd, nameOrPath);
267
+ filePath = resolve2(cwd, selectedNameOrPath);
165
268
  }
166
269
  }
167
270
  if (!statExists(filePath)) {
@@ -180,8 +283,12 @@ var runCommand = new Command().name("run").argument("<nameOrPath>", "Webhook nam
180
283
  if (options.url) def = { ...def, url: options.url };
181
284
  if (options.method)
182
285
  def = { ...def, method: options.method.toUpperCase() };
286
+ const executeSpinner = ora(
287
+ `Executing webhook: ${basename(filePath, ".json")}...`
288
+ ).start();
183
289
  try {
184
290
  const result = await executeWebhook(def);
291
+ executeSpinner.succeed("Webhook executed successfully!");
185
292
  console.log("Status:", result.status);
186
293
  console.log("Headers:");
187
294
  for (const [k, v] of Object.entries(result.headers)) {
@@ -195,7 +302,8 @@ var runCommand = new Command().name("run").argument("<nameOrPath>", "Webhook nam
195
302
  console.log(result.bodyText);
196
303
  }
197
304
  } catch (err) {
198
- console.error("Request failed:", err.message);
305
+ executeSpinner.fail("Request failed");
306
+ console.error("Error:", err.message);
199
307
  process.exitCode = 1;
200
308
  }
201
309
  });
@@ -665,7 +773,15 @@ var capture = new Command2().name("capture").description(
665
773
 
666
774
  // src/commands/replay.ts
667
775
  import { Command as Command3 } from "commander";
668
- var replay = new Command3().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(
776
+ import prompts2 from "prompts";
777
+ import ora2 from "ora";
778
+ var replay = new Command3().name("replay").argument(
779
+ "[captureId]",
780
+ "ID of the captured webhook to replay (optional - will prompt if not provided)"
781
+ ).argument(
782
+ "[targetUrl]",
783
+ "Target URL to replay the webhook to (optional - will prompt if not provided)"
784
+ ).description("Replay a captured webhook to a target URL").option("-m, --method <method>", "Override HTTP method").option(
669
785
  "-H, --header <header>",
670
786
  "Add custom header (format: key:value)",
671
787
  (value, previous) => {
@@ -680,15 +796,77 @@ var replay = new Command3().name("replay").argument("<captureId>", "ID of the ca
680
796
  },
681
797
  []
682
798
  ).action(
683
- async (captureId, targetUrl, options) => {
799
+ async (captureId, targetUrl, options = {}) => {
684
800
  const cwd = process.cwd();
685
801
  const capturesDir = findCapturesDir(cwd);
686
802
  const replayer = new WebhookReplayer(capturesDir);
687
- try {
688
- const result = await replayer.replay(captureId, targetUrl, {
689
- method: options.method,
690
- headers: options.header
803
+ const spinner = ora2("Loading captured webhooks...").start();
804
+ const captured = replayer.listCaptured();
805
+ spinner.stop();
806
+ if (captured.length === 0) {
807
+ console.log("\u{1F4ED} No captured webhooks found.");
808
+ console.log(
809
+ "\u{1F4A1} Run 'better-webhook capture' to start capturing webhooks first."
810
+ );
811
+ return;
812
+ }
813
+ let selectedCaptureId = captureId;
814
+ let selectedTargetUrl = targetUrl;
815
+ if (!selectedCaptureId) {
816
+ const choices = captured.map((c) => {
817
+ const date = new Date(c.capture.timestamp).toLocaleString();
818
+ const bodySize = c.capture.rawBody?.length ?? 0;
819
+ return {
820
+ title: `${c.capture.id} - ${c.capture.method} ${c.capture.url}`,
821
+ description: `${date} | Body: ${bodySize} bytes`,
822
+ value: c.capture.id
823
+ };
824
+ });
825
+ const response = await prompts2({
826
+ type: "select",
827
+ name: "captureId",
828
+ message: "Select a captured webhook to replay:",
829
+ choices,
830
+ initial: 0
691
831
  });
832
+ if (!response.captureId) {
833
+ console.log("\u274C No webhook selected. Exiting.");
834
+ process.exitCode = 1;
835
+ return;
836
+ }
837
+ selectedCaptureId = response.captureId;
838
+ }
839
+ if (!selectedTargetUrl) {
840
+ const response = await prompts2({
841
+ type: "text",
842
+ name: "targetUrl",
843
+ message: "Enter the target URL to replay to:",
844
+ initial: "http://localhost:3000/webhook",
845
+ validate: (value) => {
846
+ try {
847
+ new URL(value);
848
+ return true;
849
+ } catch {
850
+ return "Please enter a valid URL";
851
+ }
852
+ }
853
+ });
854
+ if (!response.targetUrl) {
855
+ console.log("\u274C No target URL provided. Exiting.");
856
+ process.exitCode = 1;
857
+ return;
858
+ }
859
+ selectedTargetUrl = response.targetUrl;
860
+ }
861
+ try {
862
+ const result = await replayer.replay(
863
+ selectedCaptureId,
864
+ selectedTargetUrl,
865
+ {
866
+ method: options.method,
867
+ headers: options.header
868
+ }
869
+ );
692
870
  console.log("\u2705 Replay completed successfully!");
693
871
  console.log("Status:", result.status);
694
872
  console.log("Headers:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-webhook/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for developing and replaying webhook payloads locally.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,18 +45,22 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "commander": "^14.0.1",
48
+ "ora": "^9.0.0",
49
+ "prompts": "^2.4.2",
48
50
  "undici": "^7.16.0",
49
51
  "zod": "^4.1.8"
50
52
  },
51
53
  "devDependencies": {
52
54
  "@types/node": "^24.3.1",
55
+ "@types/prompts": "^2.4.9",
53
56
  "tsup": "^8.5.0",
54
57
  "tsx": "^4.19.2",
55
58
  "typescript": "^5.6.3",
56
59
  "@better-webhook/typescript-config": "0.0.0"
57
60
  },
58
61
  "scripts": {
59
- "build": "tsup",
60
- "dev": "tsup --watch"
62
+ "build": "tsup --format cjs,esm --dts",
63
+ "dev": "tsup --watch",
64
+ "lint": "tsc"
61
65
  }
62
66
  }