@better-webhook/cli 3.9.0 → 3.10.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.
Files changed (56) hide show
  1. package/dist/_binary_entry.js +29 -0
  2. package/dist/commands/capture.d.ts +2 -0
  3. package/dist/commands/capture.js +33 -0
  4. package/dist/commands/captures.d.ts +2 -0
  5. package/dist/commands/captures.js +316 -0
  6. package/dist/commands/dashboard.d.ts +2 -0
  7. package/dist/commands/dashboard.js +70 -0
  8. package/dist/commands/index.d.ts +6 -0
  9. package/dist/commands/index.js +6 -0
  10. package/dist/commands/replay.d.ts +2 -0
  11. package/dist/commands/replay.js +140 -0
  12. package/dist/commands/run.d.ts +2 -0
  13. package/dist/commands/run.js +182 -0
  14. package/dist/commands/templates.d.ts +2 -0
  15. package/dist/commands/templates.js +285 -0
  16. package/dist/core/capture-server.d.ts +37 -0
  17. package/dist/core/capture-server.js +400 -0
  18. package/dist/core/capture-server.test.d.ts +1 -0
  19. package/dist/core/capture-server.test.js +86 -0
  20. package/dist/core/cli-version.d.ts +1 -0
  21. package/dist/core/cli-version.js +30 -0
  22. package/dist/core/cli-version.test.d.ts +1 -0
  23. package/dist/core/cli-version.test.js +42 -0
  24. package/dist/core/dashboard-api.d.ts +8 -0
  25. package/dist/core/dashboard-api.js +333 -0
  26. package/dist/core/dashboard-server.d.ts +24 -0
  27. package/dist/core/dashboard-server.js +224 -0
  28. package/dist/core/debug-output.d.ts +3 -0
  29. package/dist/core/debug-output.js +69 -0
  30. package/dist/core/debug-verify.d.ts +25 -0
  31. package/dist/core/debug-verify.js +253 -0
  32. package/dist/core/executor.d.ts +11 -0
  33. package/dist/core/executor.js +152 -0
  34. package/dist/core/index.d.ts +5 -0
  35. package/dist/core/index.js +5 -0
  36. package/dist/core/replay-engine.d.ts +20 -0
  37. package/dist/core/replay-engine.js +293 -0
  38. package/dist/core/replay-engine.test.d.ts +1 -0
  39. package/dist/core/replay-engine.test.js +482 -0
  40. package/dist/core/runtime-paths.d.ts +2 -0
  41. package/dist/core/runtime-paths.js +65 -0
  42. package/dist/core/runtime-paths.test.d.ts +1 -0
  43. package/dist/core/runtime-paths.test.js +50 -0
  44. package/dist/core/signature.d.ts +25 -0
  45. package/dist/core/signature.js +224 -0
  46. package/dist/core/signature.test.d.ts +1 -0
  47. package/dist/core/signature.test.js +38 -0
  48. package/dist/core/template-manager.d.ts +33 -0
  49. package/dist/core/template-manager.js +313 -0
  50. package/dist/core/template-manager.test.d.ts +1 -0
  51. package/dist/core/template-manager.test.js +236 -0
  52. package/dist/index.cjs +135 -20
  53. package/dist/index.js +123 -8
  54. package/dist/types/index.d.ts +312 -0
  55. package/dist/types/index.js +87 -0
  56. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ // Auto-generated wrapper for standalone binary
2
+ // This file embeds dashboard assets into the binary
3
+
4
+ import __file_0 from "./dashboard/index.html" with { type: "file" };
5
+ import __file_1 from "./dashboard/vite.svg" with { type: "file" };
6
+ import __file_2 from "./dashboard/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2" with { type: "file" };
7
+ import __file_3 from "./dashboard/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2" with { type: "file" };
8
+ import __file_4 from "./dashboard/assets/index-CxrRCNTh.css" with { type: "file" };
9
+ import __file_5 from "./dashboard/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2" with { type: "file" };
10
+ import __file_6 from "./dashboard/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2" with { type: "file" };
11
+ import __file_7 from "./dashboard/assets/index-Dlqdzwyc.js" with { type: "file" };
12
+ import __file_8 from "./dashboard/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2" with { type: "file" };
13
+
14
+ // Make embedded file paths available globally for runtime access
15
+ // This must be set BEFORE importing the CLI
16
+ globalThis.embeddedDashboardFiles = {
17
+ "dashboard/index.html": __file_0,
18
+ "dashboard/vite.svg": __file_1,
19
+ "dashboard/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2": __file_2,
20
+ "dashboard/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2": __file_3,
21
+ "dashboard/assets/index-CxrRCNTh.css": __file_4,
22
+ "dashboard/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2": __file_5,
23
+ "dashboard/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2": __file_6,
24
+ "dashboard/assets/index-Dlqdzwyc.js": __file_7,
25
+ "dashboard/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2": __file_8,
26
+ };
27
+
28
+ // Import and run the CLI using dynamic import to ensure globalThis is set first
29
+ await import("./index.js");
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const capture: Command;
@@ -0,0 +1,33 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { CaptureServer } from "../core/capture-server.js";
4
+ export const capture = new Command()
5
+ .name("capture")
6
+ .description("Start a server to capture incoming webhooks")
7
+ .option("-p, --port <port>", "Port to listen on", "3001")
8
+ .option("-h, --host <host>", "Host to bind to", "0.0.0.0")
9
+ .option("-v, --verbose", "Show raw request details")
10
+ .option("--debug", "Alias for --verbose")
11
+ .action(async (options) => {
12
+ const port = parseInt(options.port, 10);
13
+ if (isNaN(port) || port < 0 || port > 65535) {
14
+ console.error(chalk.red("Invalid port number"));
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+ const verbose = Boolean(options.verbose || options.debug);
19
+ const server = new CaptureServer({ verbose });
20
+ try {
21
+ await server.start(port, options.host);
22
+ const shutdown = async () => {
23
+ await server.stop();
24
+ process.exit(0);
25
+ };
26
+ process.on("SIGINT", shutdown);
27
+ process.on("SIGTERM", shutdown);
28
+ }
29
+ catch (error) {
30
+ console.error(chalk.red(`Failed to start server: ${error.message}`));
31
+ process.exitCode = 1;
32
+ }
33
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const captures: Command;
@@ -0,0 +1,316 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import prompts from "prompts";
4
+ import { homedir } from "os";
5
+ import { getReplayEngine } from "../core/replay-engine.js";
6
+ import { getTemplateManager } from "../core/template-manager.js";
7
+ function toRelativePath(absolutePath) {
8
+ const home = homedir();
9
+ if (absolutePath.startsWith(home)) {
10
+ return "~" + absolutePath.slice(home.length);
11
+ }
12
+ return absolutePath;
13
+ }
14
+ const listCommand = new Command()
15
+ .name("list")
16
+ .alias("ls")
17
+ .description("List captured webhooks")
18
+ .option("-l, --limit <limit>", "Maximum number of captures to show", "20")
19
+ .option("-p, --provider <provider>", "Filter by provider")
20
+ .action((options) => {
21
+ const limit = parseInt(options.limit, 10);
22
+ if (isNaN(limit) || limit <= 0) {
23
+ console.error(chalk.red("Invalid limit value"));
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const engine = getReplayEngine();
28
+ let captures = engine.listCaptures(limit);
29
+ if (options.provider) {
30
+ captures = captures.filter((c) => c.capture.provider?.toLowerCase() === options.provider?.toLowerCase());
31
+ }
32
+ if (captures.length === 0) {
33
+ console.log(chalk.yellow("\nšŸ“­ No captured webhooks found."));
34
+ console.log(chalk.gray(" Start capturing with: better-webhook capture\n"));
35
+ return;
36
+ }
37
+ console.log(chalk.bold("\nšŸ“¦ Captured Webhooks\n"));
38
+ for (const { file, capture } of captures) {
39
+ const date = new Date(capture.timestamp).toLocaleString();
40
+ const provider = capture.provider
41
+ ? chalk.cyan(`[${capture.provider}]`)
42
+ : chalk.gray("[unknown]");
43
+ const size = capture.contentLength || capture.rawBody?.length || 0;
44
+ console.log(` ${chalk.white(capture.id.slice(0, 8))} ${provider}`);
45
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
46
+ console.log(chalk.gray(` ${date} | ${size} bytes`));
47
+ console.log(chalk.gray(` File: ${file}`));
48
+ console.log();
49
+ }
50
+ console.log(chalk.gray(` Showing ${captures.length} captures`));
51
+ console.log(chalk.gray(` Storage: ${engine.getCapturesDir()}\n`));
52
+ });
53
+ const showCommand = new Command()
54
+ .name("show")
55
+ .argument("<captureId>", "Capture ID or partial ID")
56
+ .description("Show detailed information about a capture")
57
+ .option("-b, --body", "Show full body content")
58
+ .action((captureId, options) => {
59
+ const engine = getReplayEngine();
60
+ const captureFile = engine.getCapture(captureId);
61
+ if (!captureFile) {
62
+ console.log(chalk.red(`\nāŒ Capture not found: ${captureId}\n`));
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const { capture } = captureFile;
67
+ console.log(chalk.bold("\nšŸ“‹ Capture Details\n"));
68
+ console.log(` ${chalk.gray("ID:")} ${capture.id}`);
69
+ console.log(` ${chalk.gray("File:")} ${captureFile.file}`);
70
+ console.log(` ${chalk.gray("Timestamp:")} ${new Date(capture.timestamp).toLocaleString()}`);
71
+ console.log(` ${chalk.gray("Method:")} ${capture.method}`);
72
+ console.log(` ${chalk.gray("Path:")} ${capture.path}`);
73
+ console.log(` ${chalk.gray("URL:")} ${capture.url}`);
74
+ if (capture.provider) {
75
+ console.log(` ${chalk.gray("Provider:")} ${chalk.cyan(capture.provider)}`);
76
+ }
77
+ console.log(` ${chalk.gray("Content-Type:")} ${capture.contentType || "unknown"}`);
78
+ console.log(` ${chalk.gray("Content-Length:")} ${capture.contentLength || 0} bytes`);
79
+ const queryKeys = Object.keys(capture.query);
80
+ if (queryKeys.length > 0) {
81
+ console.log(chalk.bold("\n Query Parameters:"));
82
+ for (const [key, value] of Object.entries(capture.query)) {
83
+ const queryValue = Array.isArray(value) ? value.join(", ") : value;
84
+ console.log(chalk.gray(` ${key}: ${queryValue}`));
85
+ }
86
+ }
87
+ console.log(chalk.bold("\n Headers:"));
88
+ for (const [key, value] of Object.entries(capture.headers)) {
89
+ const headerValue = Array.isArray(value) ? value.join(", ") : value;
90
+ const display = headerValue.length > 80
91
+ ? headerValue.slice(0, 80) + "..."
92
+ : headerValue;
93
+ console.log(chalk.gray(` ${key}: ${display}`));
94
+ }
95
+ if (options.body && capture.body) {
96
+ console.log(chalk.bold("\n Body:"));
97
+ if (typeof capture.body === "object") {
98
+ console.log(chalk.gray(JSON.stringify(capture.body, null, 2)
99
+ .split("\n")
100
+ .map((l) => ` ${l}`)
101
+ .join("\n")));
102
+ }
103
+ else {
104
+ console.log(chalk.gray(` ${capture.body}`));
105
+ }
106
+ }
107
+ else if (capture.body) {
108
+ console.log(chalk.bold("\n Body:"));
109
+ const preview = JSON.stringify(capture.body).slice(0, 200);
110
+ console.log(chalk.gray(` ${preview}${preview.length >= 200 ? "..." : ""}`));
111
+ console.log(chalk.gray(" Use --body to see full content"));
112
+ }
113
+ console.log();
114
+ });
115
+ const searchCommand = new Command()
116
+ .name("search")
117
+ .argument("<query>", "Search query")
118
+ .description("Search captures by path, method, or provider")
119
+ .action((query) => {
120
+ const engine = getReplayEngine();
121
+ const results = engine.searchCaptures(query);
122
+ if (results.length === 0) {
123
+ console.log(chalk.yellow(`\nšŸ“­ No captures found for: "${query}"\n`));
124
+ return;
125
+ }
126
+ console.log(chalk.bold(`\nšŸ” Search Results for "${query}"\n`));
127
+ for (const { file, capture } of results) {
128
+ const date = new Date(capture.timestamp).toLocaleString();
129
+ const provider = capture.provider
130
+ ? chalk.cyan(`[${capture.provider}]`)
131
+ : "";
132
+ console.log(` ${chalk.white(capture.id.slice(0, 8))} ${provider}`);
133
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
134
+ console.log(chalk.gray(` ${date}`));
135
+ console.log();
136
+ }
137
+ console.log(chalk.gray(` Found: ${results.length} captures\n`));
138
+ });
139
+ const deleteCommand = new Command()
140
+ .name("delete")
141
+ .alias("rm")
142
+ .argument("<captureId>", "Capture ID or partial ID to delete")
143
+ .description("Delete a specific captured webhook")
144
+ .option("-f, --force", "Skip confirmation prompt")
145
+ .action(async (captureId, options) => {
146
+ const engine = getReplayEngine();
147
+ const captureFile = engine.getCapture(captureId);
148
+ if (!captureFile) {
149
+ console.log(chalk.red(`\nāŒ Capture not found: ${captureId}\n`));
150
+ process.exitCode = 1;
151
+ return;
152
+ }
153
+ const { capture } = captureFile;
154
+ if (!options.force) {
155
+ console.log(chalk.bold("\nšŸ—‘ļø Capture to delete:\n"));
156
+ console.log(` ${chalk.white(capture.id.slice(0, 8))}`);
157
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
158
+ console.log(chalk.gray(` ${new Date(capture.timestamp).toLocaleString()}`));
159
+ console.log();
160
+ const response = await prompts({
161
+ type: "confirm",
162
+ name: "confirm",
163
+ message: "Delete this capture?",
164
+ initial: false,
165
+ });
166
+ if (!response.confirm) {
167
+ console.log(chalk.yellow("Cancelled"));
168
+ return;
169
+ }
170
+ }
171
+ const deleted = engine.deleteCapture(captureId);
172
+ if (deleted) {
173
+ console.log(chalk.green(`\nāœ“ Deleted capture: ${capture.id.slice(0, 8)}\n`));
174
+ }
175
+ else {
176
+ console.log(chalk.red(`\nāŒ Failed to delete capture\n`));
177
+ process.exitCode = 1;
178
+ }
179
+ });
180
+ const cleanCommand = new Command()
181
+ .name("clean")
182
+ .alias("remove-all")
183
+ .description("Remove all captured webhooks")
184
+ .option("-f, --force", "Skip confirmation prompt")
185
+ .action(async (options) => {
186
+ const engine = getReplayEngine();
187
+ const captures = engine.listCaptures(10000);
188
+ if (captures.length === 0) {
189
+ console.log(chalk.yellow("\nšŸ“­ No captures to remove.\n"));
190
+ return;
191
+ }
192
+ console.log(chalk.bold(`\nšŸ—‘ļø Found ${captures.length} captured webhook(s)\n`));
193
+ const byProvider = new Map();
194
+ for (const c of captures) {
195
+ const provider = c.capture.provider || "unknown";
196
+ byProvider.set(provider, (byProvider.get(provider) || 0) + 1);
197
+ }
198
+ for (const [provider, count] of byProvider) {
199
+ console.log(chalk.gray(` ${provider}: ${count}`));
200
+ }
201
+ console.log();
202
+ if (!options.force) {
203
+ const response = await prompts({
204
+ type: "confirm",
205
+ name: "confirm",
206
+ message: `Delete all ${captures.length} capture(s)?`,
207
+ initial: false,
208
+ });
209
+ if (!response.confirm) {
210
+ console.log(chalk.yellow("Cancelled"));
211
+ return;
212
+ }
213
+ }
214
+ const deleted = engine.deleteAllCaptures();
215
+ console.log(chalk.green(`\nāœ“ Removed ${deleted} capture(s)`));
216
+ console.log(chalk.gray(` Storage: ${engine.getCapturesDir()}\n`));
217
+ });
218
+ const saveAsTemplateCommand = new Command()
219
+ .name("save-as-template")
220
+ .alias("sat")
221
+ .argument("<captureId>", "Capture ID or partial ID")
222
+ .description("Save a captured webhook as a reusable template")
223
+ .option("--id <id>", "Template ID (auto-generated if not provided)")
224
+ .option("--name <name>", "Template display name")
225
+ .option("--event <event>", "Event type (auto-detected if not provided)")
226
+ .option("--description <description>", "Template description")
227
+ .option("--url <url>", "Default target URL for the template")
228
+ .option("--overwrite", "Overwrite existing template with same ID")
229
+ .action(async (captureId, options) => {
230
+ const engine = getReplayEngine();
231
+ const templateManager = getTemplateManager();
232
+ const captureFile = engine.getCapture(captureId);
233
+ if (!captureFile) {
234
+ console.log(chalk.red(`\nāŒ Capture not found: ${captureId}\n`));
235
+ process.exitCode = 1;
236
+ return;
237
+ }
238
+ const { capture } = captureFile;
239
+ console.log(chalk.bold("\nšŸ“‹ Capture to save as template:\n"));
240
+ console.log(` ${chalk.white(capture.id.slice(0, 8))}`);
241
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
242
+ if (capture.provider) {
243
+ console.log(chalk.gray(` Provider: ${capture.provider}`));
244
+ }
245
+ console.log();
246
+ const template = engine.captureToTemplate(captureId, {
247
+ url: options.url,
248
+ event: options.event,
249
+ });
250
+ let templateId = options.id;
251
+ if (!templateId) {
252
+ const suggestedId = `${capture.provider || "custom"}-${template.event || "webhook"}`
253
+ .toLowerCase()
254
+ .replace(/\s+/g, "-");
255
+ const response = await prompts({
256
+ type: "text",
257
+ name: "templateId",
258
+ message: "Template ID:",
259
+ initial: suggestedId,
260
+ validate: (value) => value.trim().length > 0 || "Template ID is required",
261
+ });
262
+ if (!response.templateId) {
263
+ console.log(chalk.yellow("Cancelled"));
264
+ return;
265
+ }
266
+ templateId = response.templateId;
267
+ }
268
+ if (!options.overwrite &&
269
+ templateId &&
270
+ templateManager.templateExists(templateId)) {
271
+ const response = await prompts({
272
+ type: "confirm",
273
+ name: "overwrite",
274
+ message: `Template "${templateId}" already exists. Overwrite?`,
275
+ initial: false,
276
+ });
277
+ if (!response.overwrite) {
278
+ console.log(chalk.yellow("Cancelled"));
279
+ return;
280
+ }
281
+ options.overwrite = true;
282
+ }
283
+ try {
284
+ const result = templateManager.saveUserTemplate(template, {
285
+ id: templateId,
286
+ name: options.name,
287
+ event: options.event || template.event,
288
+ description: options.description,
289
+ overwrite: options.overwrite,
290
+ });
291
+ console.log(chalk.green(`\nāœ“ Saved template: ${result.id}`));
292
+ console.log(chalk.gray(` File: ${toRelativePath(result.filePath)}`));
293
+ console.log(chalk.gray(` Provider: ${template.provider || "custom"}`));
294
+ if (template.event) {
295
+ console.log(chalk.gray(` Event: ${template.event}`));
296
+ }
297
+ console.log();
298
+ console.log(chalk.gray(" Run it with:"));
299
+ console.log(chalk.cyan(` better-webhook run ${result.id} --url http://localhost:3000/webhooks\n`));
300
+ }
301
+ catch (error) {
302
+ const message = error instanceof Error ? error.message : "Failed to save template";
303
+ console.log(chalk.red(`\nāŒ ${message}\n`));
304
+ process.exitCode = 1;
305
+ }
306
+ });
307
+ export const captures = new Command()
308
+ .name("captures")
309
+ .alias("c")
310
+ .description("Manage captured webhooks")
311
+ .addCommand(listCommand)
312
+ .addCommand(showCommand)
313
+ .addCommand(searchCommand)
314
+ .addCommand(deleteCommand)
315
+ .addCommand(cleanCommand)
316
+ .addCommand(saveAsTemplateCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const dashboard: Command;
@@ -0,0 +1,70 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { startDashboardServer } from "../core/dashboard-server.js";
4
+ export const dashboard = new Command()
5
+ .name("dashboard")
6
+ .description("Start the local dashboard (UI + API + WebSocket) server")
7
+ .option("-p, --port <port>", "Port to listen on", "4000")
8
+ .option("-h, --host <host>", "Host to bind to", "localhost")
9
+ .option("-v, --verbose", "Show dashboard startup debug details")
10
+ .option("--debug", "Alias for --verbose")
11
+ .option("--capture-port <port>", "Capture server port", "3001")
12
+ .option("--capture-host <host>", "Capture server host", "0.0.0.0")
13
+ .option("--no-capture", "Do not start the capture server")
14
+ .option("--captures-dir <dir>", "Override captures directory")
15
+ .option("--templates-dir <dir>", "Override templates base directory")
16
+ .action(async (options) => {
17
+ const port = Number.parseInt(String(options.port), 10);
18
+ if (!Number.isFinite(port) || port < 0 || port > 65535) {
19
+ console.error(chalk.red("Invalid port number"));
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ const capturePort = Number.parseInt(String(options.capturePort), 10);
24
+ if (!Number.isFinite(capturePort) ||
25
+ capturePort < 0 ||
26
+ capturePort > 65535) {
27
+ console.error(chalk.red("Invalid capture port number"));
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ try {
32
+ const verbose = Boolean(options.verbose || options.debug);
33
+ const { url, server, capture } = await startDashboardServer({
34
+ host: options.host,
35
+ port,
36
+ captureHost: options.captureHost,
37
+ capturePort,
38
+ startCapture: options.capture !== false,
39
+ capturesDir: options.capturesDir,
40
+ templatesBaseDir: options.templatesDir,
41
+ verbose,
42
+ });
43
+ console.log(chalk.bold("\n🧭 Dashboard Server\n"));
44
+ console.log(chalk.gray(` Dashboard: ${url}/`));
45
+ console.log(chalk.gray(` Health: ${url}/health`));
46
+ console.log(chalk.gray(` API Base: ${url}/api`));
47
+ console.log(chalk.gray(` WebSocket: ${url.replace("http://", "ws://")}/ws`));
48
+ if (capture) {
49
+ console.log();
50
+ console.log(chalk.bold("šŸŽ£ Capture Server"));
51
+ console.log(chalk.gray(` Capture: ${capture.url}`));
52
+ console.log(chalk.gray(` Tip: Send webhooks to any path, e.g. ${capture.url}/webhooks/github`));
53
+ }
54
+ console.log();
55
+ const shutdown = async () => {
56
+ if (capture) {
57
+ await capture.server.stop();
58
+ }
59
+ await new Promise((resolve) => server.close(() => resolve()));
60
+ process.exit(0);
61
+ };
62
+ process.on("SIGINT", shutdown);
63
+ process.on("SIGTERM", shutdown);
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ console.error(chalk.red(`Failed to start dashboard server: ${message}`));
68
+ process.exitCode = 1;
69
+ }
70
+ });
@@ -0,0 +1,6 @@
1
+ export { templates } from "./templates.js";
2
+ export { run } from "./run.js";
3
+ export { capture } from "./capture.js";
4
+ export { captures } from "./captures.js";
5
+ export { replay } from "./replay.js";
6
+ export { dashboard } from "./dashboard.js";
@@ -0,0 +1,6 @@
1
+ export { templates } from "./templates.js";
2
+ export { run } from "./run.js";
3
+ export { capture } from "./capture.js";
4
+ export { captures } from "./captures.js";
5
+ export { replay } from "./replay.js";
6
+ export { dashboard } from "./dashboard.js";
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const replay: Command;
@@ -0,0 +1,140 @@
1
+ import { Command } from "commander";
2
+ import ora from "ora";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import { getReplayEngine } from "../core/replay-engine.js";
6
+ export const replay = new Command()
7
+ .name("replay")
8
+ .argument("[captureId]", "Capture ID to replay")
9
+ .argument("[targetUrl]", "Target URL to replay to")
10
+ .description("Replay a captured webhook to a target URL")
11
+ .option("-m, --method <method>", "Override HTTP method")
12
+ .option("-H, --header <header>", "Add or override header (format: key:value)", (value, previous) => {
13
+ const [key, ...valueParts] = value.split(":");
14
+ const headerValue = valueParts.join(":");
15
+ if (!key || !headerValue) {
16
+ throw new Error("Header format should be key:value");
17
+ }
18
+ return (previous || []).concat([
19
+ { key: key.trim(), value: headerValue.trim() },
20
+ ]);
21
+ }, [])
22
+ .option("-v, --verbose", "Show detailed request/response information")
23
+ .action(async (captureId, targetUrl, options) => {
24
+ const engine = getReplayEngine();
25
+ if (!captureId) {
26
+ const captures = engine.listCaptures(50);
27
+ if (captures.length === 0) {
28
+ console.log(chalk.yellow("\nšŸ“­ No captured webhooks found."));
29
+ console.log(chalk.gray(" Start capturing with: better-webhook capture\n"));
30
+ return;
31
+ }
32
+ const choices = captures.map((c) => {
33
+ const date = new Date(c.capture.timestamp).toLocaleString();
34
+ const provider = c.capture.provider ? `[${c.capture.provider}]` : "";
35
+ return {
36
+ title: `${c.capture.id.slice(0, 8)} ${provider} ${c.capture.method} ${c.capture.path}`,
37
+ description: date,
38
+ value: c.capture.id,
39
+ };
40
+ });
41
+ const response = await prompts({
42
+ type: "select",
43
+ name: "captureId",
44
+ message: "Select a capture to replay:",
45
+ choices,
46
+ });
47
+ if (!response.captureId) {
48
+ console.log(chalk.yellow("Cancelled"));
49
+ return;
50
+ }
51
+ captureId = response.captureId;
52
+ }
53
+ const captureFile = engine.getCapture(captureId);
54
+ if (!captureFile) {
55
+ console.log(chalk.red(`\nāŒ Capture not found: ${captureId}\n`));
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ if (!targetUrl) {
60
+ const response = await prompts({
61
+ type: "text",
62
+ name: "url",
63
+ message: "Enter target URL:",
64
+ initial: `http://localhost:3000${captureFile.capture.path}`,
65
+ validate: (value) => {
66
+ try {
67
+ new URL(value);
68
+ return true;
69
+ }
70
+ catch {
71
+ return "Please enter a valid URL";
72
+ }
73
+ },
74
+ });
75
+ if (!response.url) {
76
+ console.log(chalk.yellow("Cancelled"));
77
+ return;
78
+ }
79
+ targetUrl = response.url;
80
+ }
81
+ const { capture } = captureFile;
82
+ console.log(chalk.bold("\nšŸ”„ Replaying Webhook\n"));
83
+ console.log(chalk.gray(` Capture ID: ${capture.id.slice(0, 8)}`));
84
+ console.log(chalk.gray(` Original: ${capture.method} ${capture.path}`));
85
+ if (capture.provider) {
86
+ console.log(chalk.gray(` Provider: ${capture.provider}`));
87
+ }
88
+ console.log(chalk.gray(` Target: ${targetUrl}`));
89
+ console.log();
90
+ const spinner = ora("Replaying webhook...").start();
91
+ try {
92
+ const result = await engine.replay(captureId, {
93
+ targetUrl: targetUrl,
94
+ method: options?.method,
95
+ headers: options?.header,
96
+ });
97
+ spinner.stop();
98
+ const statusColor = result.status >= 200 && result.status < 300
99
+ ? chalk.green
100
+ : result.status >= 400
101
+ ? chalk.red
102
+ : chalk.yellow;
103
+ console.log(chalk.bold("šŸ“„ Response\n"));
104
+ console.log(` Status: ${statusColor(`${result.status} ${result.statusText}`)}`);
105
+ console.log(` Duration: ${chalk.cyan(`${result.duration}ms`)}`);
106
+ if (options?.verbose) {
107
+ console.log(chalk.bold("\n Headers:"));
108
+ for (const [key, value] of Object.entries(result.headers)) {
109
+ const headerValue = Array.isArray(value) ? value.join(", ") : value;
110
+ console.log(chalk.gray(` ${key}: ${headerValue}`));
111
+ }
112
+ }
113
+ if (result.json !== undefined) {
114
+ console.log(chalk.bold("\n Body:"));
115
+ console.log(chalk.gray(JSON.stringify(result.json, null, 2)
116
+ .split("\n")
117
+ .map((l) => ` ${l}`)
118
+ .join("\n")));
119
+ }
120
+ else if (result.bodyText) {
121
+ console.log(chalk.bold("\n Body:"));
122
+ const preview = result.bodyText.length > 500
123
+ ? result.bodyText.slice(0, 500) + "..."
124
+ : result.bodyText;
125
+ console.log(chalk.gray(` ${preview}`));
126
+ }
127
+ console.log();
128
+ if (result.status >= 200 && result.status < 300) {
129
+ console.log(chalk.green("āœ“ Replay completed successfully\n"));
130
+ }
131
+ else {
132
+ console.log(chalk.yellow(`⚠ Replay completed with status ${result.status}\n`));
133
+ }
134
+ }
135
+ catch (error) {
136
+ spinner.fail("Replay failed");
137
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
138
+ process.exitCode = 1;
139
+ }
140
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const run: Command;