@better-webhook/cli 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,10 @@ A modern CLI tool for webhook development, testing, and debugging. Capture incom
18
18
 
19
19
  ## Supported Providers
20
20
 
21
+ ### Signature Generation Support
22
+
23
+ The CLI can automatically generate signatures for the following providers when you provide a `--secret`:
24
+
21
25
  | Provider | Signature Algorithm | Auto-Detection |
22
26
  | ------------ | ------------------------------- | -------------- |
23
27
  | Stripe | HMAC-SHA256 (`t={ts},v1={sig}`) | ✅ |
@@ -28,8 +32,16 @@ A modern CLI tool for webhook development, testing, and debugging. Capture incom
28
32
  | SendGrid | HMAC-SHA256 (Base64) | ✅ |
29
33
  | Linear | HMAC-SHA256 (Hex) | ✅ |
30
34
  | Clerk (Svix) | HMAC-SHA256 (`v1,{sig}`) | ✅ |
31
- | Discord | Ed25519 | ✅ |
32
- | Custom | — | — |
35
+ | Ragie | HMAC-SHA256 (Hex) | ✅ |
36
+
37
+ ### Detection Only (No Signature Generation)
38
+
39
+ These providers are auto-detected from headers but signature generation is not yet implemented:
40
+
41
+ | Provider | Notes |
42
+ | -------- | ----------------------------------------------------- |
43
+ | Discord | Detected from headers; uses Ed25519 (not implemented) |
44
+ | Custom | Generic detection; no signature generation |
33
45
 
34
46
  ## Installation
35
47
 
@@ -51,7 +63,6 @@ npx @better-webhook/cli --help
51
63
 
52
64
  ```bash
53
65
  better-webhook --version
54
- # 2.0.0
55
66
  ```
56
67
 
57
68
  ## Quick Start
@@ -379,15 +390,15 @@ By default, this command starts:
379
390
  better-webhook dashboard [options]
380
391
  ```
381
392
 
382
- | Option | Description | Default |
383
- | ------------------------- | ------------------------------- | ----------- |
384
- | `-p, --port <port>` | Dashboard server port | `4000` |
385
- | `-h, --host <host>` | Dashboard server host | `localhost` |
386
- | `--capture-port <port>` | Capture server port | `3001` |
387
- | `--capture-host <host>` | Capture server host | `0.0.0.0` |
388
- | `--no-capture` | Do not start capture server | — |
389
- | `--captures-dir <dir>` | Override captures directory | — |
390
- | `--templates-dir <dir>` | Override templates base dir | — |
393
+ | Option | Description | Default |
394
+ | ----------------------- | --------------------------- | ----------- |
395
+ | `-p, --port <port>` | Dashboard server port | `4000` |
396
+ | `-h, --host <host>` | Dashboard server host | `localhost` |
397
+ | `--capture-port <port>` | Capture server port | `3001` |
398
+ | `--capture-host <host>` | Capture server host | `0.0.0.0` |
399
+ | `--no-capture` | Do not start capture server | — |
400
+ | `--captures-dir <dir>` | Override captures directory | — |
401
+ | `--templates-dir <dir>` | Override templates base dir | — |
391
402
 
392
403
  **Security note:**
393
404
  Keep the dashboard bound to `localhost` unless you trust your network. The API includes endpoints that can send HTTP requests to arbitrary URLs (run/replay).
@@ -408,6 +419,7 @@ The CLI automatically reads webhook secrets from environment variables based on
408
419
  | Linear | `LINEAR_WEBHOOK_SECRET` |
409
420
  | Clerk | `CLERK_WEBHOOK_SECRET` |
410
421
  | SendGrid | `SENDGRID_WEBHOOK_SECRET` |
422
+ | Ragie | `RAGIE_WEBHOOK_SECRET` |
411
423
  | Discord | `DISCORD_WEBHOOK_SECRET` |
412
424
  | Custom | `WEBHOOK_SECRET` |
413
425
 
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const capture: Command;
@@ -0,0 +1,30 @@
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
+ .action(async (options) => {
10
+ const port = parseInt(options.port, 10);
11
+ if (isNaN(port) || port < 0 || port > 65535) {
12
+ console.error(chalk.red("Invalid port number"));
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ const server = new CaptureServer();
17
+ try {
18
+ await server.start(port, options.host);
19
+ const shutdown = async () => {
20
+ await server.stop();
21
+ process.exit(0);
22
+ };
23
+ process.on("SIGINT", shutdown);
24
+ process.on("SIGTERM", shutdown);
25
+ }
26
+ catch (error) {
27
+ console.error(chalk.red(`Failed to start server: ${error.message}`));
28
+ process.exitCode = 1;
29
+ }
30
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const captures: Command;
@@ -0,0 +1,217 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import prompts from "prompts";
4
+ import { getReplayEngine } from "../core/replay-engine.js";
5
+ const listCommand = new Command()
6
+ .name("list")
7
+ .alias("ls")
8
+ .description("List captured webhooks")
9
+ .option("-l, --limit <limit>", "Maximum number of captures to show", "20")
10
+ .option("-p, --provider <provider>", "Filter by provider")
11
+ .action((options) => {
12
+ const limit = parseInt(options.limit, 10);
13
+ if (isNaN(limit) || limit <= 0) {
14
+ console.error(chalk.red("Invalid limit value"));
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+ const engine = getReplayEngine();
19
+ let captures = engine.listCaptures(limit);
20
+ if (options.provider) {
21
+ captures = captures.filter((c) => c.capture.provider?.toLowerCase() === options.provider?.toLowerCase());
22
+ }
23
+ if (captures.length === 0) {
24
+ console.log(chalk.yellow("\n📭 No captured webhooks found."));
25
+ console.log(chalk.gray(" Start capturing with: better-webhook capture\n"));
26
+ return;
27
+ }
28
+ console.log(chalk.bold("\n📦 Captured Webhooks\n"));
29
+ for (const { file, capture } of captures) {
30
+ const date = new Date(capture.timestamp).toLocaleString();
31
+ const provider = capture.provider
32
+ ? chalk.cyan(`[${capture.provider}]`)
33
+ : chalk.gray("[unknown]");
34
+ const size = capture.contentLength || capture.rawBody?.length || 0;
35
+ console.log(` ${chalk.white(capture.id.slice(0, 8))} ${provider}`);
36
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
37
+ console.log(chalk.gray(` ${date} | ${size} bytes`));
38
+ console.log(chalk.gray(` File: ${file}`));
39
+ console.log();
40
+ }
41
+ console.log(chalk.gray(` Showing ${captures.length} captures`));
42
+ console.log(chalk.gray(` Storage: ${engine.getCapturesDir()}\n`));
43
+ });
44
+ const showCommand = new Command()
45
+ .name("show")
46
+ .argument("<captureId>", "Capture ID or partial ID")
47
+ .description("Show detailed information about a capture")
48
+ .option("-b, --body", "Show full body content")
49
+ .action((captureId, options) => {
50
+ const engine = getReplayEngine();
51
+ const captureFile = engine.getCapture(captureId);
52
+ if (!captureFile) {
53
+ console.log(chalk.red(`\n❌ Capture not found: ${captureId}\n`));
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ const { capture } = captureFile;
58
+ console.log(chalk.bold("\n📋 Capture Details\n"));
59
+ console.log(` ${chalk.gray("ID:")} ${capture.id}`);
60
+ console.log(` ${chalk.gray("File:")} ${captureFile.file}`);
61
+ console.log(` ${chalk.gray("Timestamp:")} ${new Date(capture.timestamp).toLocaleString()}`);
62
+ console.log(` ${chalk.gray("Method:")} ${capture.method}`);
63
+ console.log(` ${chalk.gray("Path:")} ${capture.path}`);
64
+ console.log(` ${chalk.gray("URL:")} ${capture.url}`);
65
+ if (capture.provider) {
66
+ console.log(` ${chalk.gray("Provider:")} ${chalk.cyan(capture.provider)}`);
67
+ }
68
+ console.log(` ${chalk.gray("Content-Type:")} ${capture.contentType || "unknown"}`);
69
+ console.log(` ${chalk.gray("Content-Length:")} ${capture.contentLength || 0} bytes`);
70
+ const queryKeys = Object.keys(capture.query);
71
+ if (queryKeys.length > 0) {
72
+ console.log(chalk.bold("\n Query Parameters:"));
73
+ for (const [key, value] of Object.entries(capture.query)) {
74
+ const queryValue = Array.isArray(value) ? value.join(", ") : value;
75
+ console.log(chalk.gray(` ${key}: ${queryValue}`));
76
+ }
77
+ }
78
+ console.log(chalk.bold("\n Headers:"));
79
+ for (const [key, value] of Object.entries(capture.headers)) {
80
+ const headerValue = Array.isArray(value) ? value.join(", ") : value;
81
+ const display = headerValue.length > 80
82
+ ? headerValue.slice(0, 80) + "..."
83
+ : headerValue;
84
+ console.log(chalk.gray(` ${key}: ${display}`));
85
+ }
86
+ if (options.body && capture.body) {
87
+ console.log(chalk.bold("\n Body:"));
88
+ if (typeof capture.body === "object") {
89
+ console.log(chalk.gray(JSON.stringify(capture.body, null, 2)
90
+ .split("\n")
91
+ .map((l) => ` ${l}`)
92
+ .join("\n")));
93
+ }
94
+ else {
95
+ console.log(chalk.gray(` ${capture.body}`));
96
+ }
97
+ }
98
+ else if (capture.body) {
99
+ console.log(chalk.bold("\n Body:"));
100
+ const preview = JSON.stringify(capture.body).slice(0, 200);
101
+ console.log(chalk.gray(` ${preview}${preview.length >= 200 ? "..." : ""}`));
102
+ console.log(chalk.gray(" Use --body to see full content"));
103
+ }
104
+ console.log();
105
+ });
106
+ const searchCommand = new Command()
107
+ .name("search")
108
+ .argument("<query>", "Search query")
109
+ .description("Search captures by path, method, or provider")
110
+ .action((query) => {
111
+ const engine = getReplayEngine();
112
+ const results = engine.searchCaptures(query);
113
+ if (results.length === 0) {
114
+ console.log(chalk.yellow(`\n📭 No captures found for: "${query}"\n`));
115
+ return;
116
+ }
117
+ console.log(chalk.bold(`\n🔍 Search Results for "${query}"\n`));
118
+ for (const { file, capture } of results) {
119
+ const date = new Date(capture.timestamp).toLocaleString();
120
+ const provider = capture.provider
121
+ ? chalk.cyan(`[${capture.provider}]`)
122
+ : "";
123
+ console.log(` ${chalk.white(capture.id.slice(0, 8))} ${provider}`);
124
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
125
+ console.log(chalk.gray(` ${date}`));
126
+ console.log();
127
+ }
128
+ console.log(chalk.gray(` Found: ${results.length} captures\n`));
129
+ });
130
+ const deleteCommand = new Command()
131
+ .name("delete")
132
+ .alias("rm")
133
+ .argument("<captureId>", "Capture ID or partial ID to delete")
134
+ .description("Delete a specific captured webhook")
135
+ .option("-f, --force", "Skip confirmation prompt")
136
+ .action(async (captureId, options) => {
137
+ const engine = getReplayEngine();
138
+ const captureFile = engine.getCapture(captureId);
139
+ if (!captureFile) {
140
+ console.log(chalk.red(`\n❌ Capture not found: ${captureId}\n`));
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ const { capture } = captureFile;
145
+ if (!options.force) {
146
+ console.log(chalk.bold("\n🗑️ Capture to delete:\n"));
147
+ console.log(` ${chalk.white(capture.id.slice(0, 8))}`);
148
+ console.log(chalk.gray(` ${capture.method} ${capture.path}`));
149
+ console.log(chalk.gray(` ${new Date(capture.timestamp).toLocaleString()}`));
150
+ console.log();
151
+ const response = await prompts({
152
+ type: "confirm",
153
+ name: "confirm",
154
+ message: "Delete this capture?",
155
+ initial: false,
156
+ });
157
+ if (!response.confirm) {
158
+ console.log(chalk.yellow("Cancelled"));
159
+ return;
160
+ }
161
+ }
162
+ const deleted = engine.deleteCapture(captureId);
163
+ if (deleted) {
164
+ console.log(chalk.green(`\n✓ Deleted capture: ${capture.id.slice(0, 8)}\n`));
165
+ }
166
+ else {
167
+ console.log(chalk.red(`\n❌ Failed to delete capture\n`));
168
+ process.exitCode = 1;
169
+ }
170
+ });
171
+ const cleanCommand = new Command()
172
+ .name("clean")
173
+ .alias("remove-all")
174
+ .description("Remove all captured webhooks")
175
+ .option("-f, --force", "Skip confirmation prompt")
176
+ .action(async (options) => {
177
+ const engine = getReplayEngine();
178
+ const captures = engine.listCaptures(10000);
179
+ if (captures.length === 0) {
180
+ console.log(chalk.yellow("\n📭 No captures to remove.\n"));
181
+ return;
182
+ }
183
+ console.log(chalk.bold(`\n🗑️ Found ${captures.length} captured webhook(s)\n`));
184
+ const byProvider = new Map();
185
+ for (const c of captures) {
186
+ const provider = c.capture.provider || "unknown";
187
+ byProvider.set(provider, (byProvider.get(provider) || 0) + 1);
188
+ }
189
+ for (const [provider, count] of byProvider) {
190
+ console.log(chalk.gray(` ${provider}: ${count}`));
191
+ }
192
+ console.log();
193
+ if (!options.force) {
194
+ const response = await prompts({
195
+ type: "confirm",
196
+ name: "confirm",
197
+ message: `Delete all ${captures.length} capture(s)?`,
198
+ initial: false,
199
+ });
200
+ if (!response.confirm) {
201
+ console.log(chalk.yellow("Cancelled"));
202
+ return;
203
+ }
204
+ }
205
+ const deleted = engine.deleteAllCaptures();
206
+ console.log(chalk.green(`\n✓ Removed ${deleted} capture(s)`));
207
+ console.log(chalk.gray(` Storage: ${engine.getCapturesDir()}\n`));
208
+ });
209
+ export const captures = new Command()
210
+ .name("captures")
211
+ .alias("c")
212
+ .description("Manage captured webhooks")
213
+ .addCommand(listCommand)
214
+ .addCommand(showCommand)
215
+ .addCommand(searchCommand)
216
+ .addCommand(deleteCommand)
217
+ .addCommand(cleanCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const dashboard: Command;
@@ -0,0 +1,63 @@
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("--capture-port <port>", "Capture server port", "3001")
10
+ .option("--capture-host <host>", "Capture server host", "0.0.0.0")
11
+ .option("--no-capture", "Do not start the capture server")
12
+ .option("--captures-dir <dir>", "Override captures directory")
13
+ .option("--templates-dir <dir>", "Override templates base directory")
14
+ .action(async (options) => {
15
+ const port = Number.parseInt(String(options.port), 10);
16
+ if (!Number.isFinite(port) || port < 0 || port > 65535) {
17
+ console.error(chalk.red("Invalid port number"));
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ try {
22
+ const capturePort = Number.parseInt(String(options.capturePort), 10);
23
+ if (!Number.isFinite(capturePort) || capturePort < 0 || capturePort > 65535) {
24
+ console.error(chalk.red("Invalid capture port number"));
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ const { url, server, capture } = await startDashboardServer({
29
+ host: options.host,
30
+ port,
31
+ captureHost: options.captureHost,
32
+ capturePort,
33
+ startCapture: options.capture !== false,
34
+ capturesDir: options.capturesDir,
35
+ templatesBaseDir: options.templatesDir,
36
+ });
37
+ console.log(chalk.bold("\n🧭 Dashboard Server\n"));
38
+ console.log(chalk.gray(` Dashboard: ${url}/`));
39
+ console.log(chalk.gray(` Health: ${url}/health`));
40
+ console.log(chalk.gray(` API Base: ${url}/api`));
41
+ console.log(chalk.gray(` WebSocket: ${url.replace("http://", "ws://")}/ws`));
42
+ if (capture) {
43
+ console.log();
44
+ console.log(chalk.bold("🎣 Capture Server"));
45
+ console.log(chalk.gray(` Capture: ${capture.url}`));
46
+ console.log(chalk.gray(` Tip: Send webhooks to any path, e.g. ${capture.url}/webhooks/github`));
47
+ }
48
+ console.log();
49
+ const shutdown = async () => {
50
+ if (capture) {
51
+ await capture.server.stop();
52
+ }
53
+ await new Promise((resolve) => server.close(() => resolve()));
54
+ process.exit(0);
55
+ };
56
+ process.on("SIGINT", shutdown);
57
+ process.on("SIGTERM", shutdown);
58
+ }
59
+ catch (error) {
60
+ console.error(chalk.red(`Failed to start dashboard server: ${error?.message || error}`));
61
+ process.exitCode = 1;
62
+ }
63
+ });
@@ -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;