@better-webhook/cli 3.5.0 → 3.7.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 +41 -4
- package/dist/index.cjs +180 -39
- package/dist/index.js +181 -40
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -43,6 +43,37 @@ These providers are auto-detected from headers but signature generation is not y
|
|
|
43
43
|
|
|
44
44
|
## Installation
|
|
45
45
|
|
|
46
|
+
### Standalone Binary (Recommended)
|
|
47
|
+
|
|
48
|
+
Download a standalone binary - no Node.js required:
|
|
49
|
+
|
|
50
|
+
**macOS (Homebrew)**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
brew install endalk200/tap/better-webhook
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Manual Download**
|
|
57
|
+
|
|
58
|
+
Download the latest binary for your platform from [GitHub Releases](https://github.com/endalk200/better-webhook/releases):
|
|
59
|
+
|
|
60
|
+
| Platform | Download |
|
|
61
|
+
| ------------- | --------------------------------------------------------------------------------------------- |
|
|
62
|
+
| macOS (ARM) | [better-webhook-darwin-arm64](https://github.com/endalk200/better-webhook/releases/latest) |
|
|
63
|
+
| macOS (Intel) | [better-webhook-darwin-x64](https://github.com/endalk200/better-webhook/releases/latest) |
|
|
64
|
+
| Linux (x64) | [better-webhook-linux-x64](https://github.com/endalk200/better-webhook/releases/latest) |
|
|
65
|
+
| Linux (ARM) | [better-webhook-linux-arm64](https://github.com/endalk200/better-webhook/releases/latest) |
|
|
66
|
+
| Windows | [better-webhook-windows-x64.exe](https://github.com/endalk200/better-webhook/releases/latest) |
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Example: macOS ARM
|
|
70
|
+
curl -L https://github.com/endalk200/better-webhook/releases/latest/download/better-webhook-darwin-arm64 -o better-webhook
|
|
71
|
+
chmod +x better-webhook
|
|
72
|
+
sudo mv better-webhook /usr/local/bin/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### NPM (Alternative)
|
|
76
|
+
|
|
46
77
|
```bash
|
|
47
78
|
# NPM
|
|
48
79
|
npm install -g @better-webhook/cli
|
|
@@ -134,10 +165,12 @@ Start a server to capture incoming webhooks. All captured webhooks are saved to
|
|
|
134
165
|
better-webhook capture [options]
|
|
135
166
|
```
|
|
136
167
|
|
|
137
|
-
| Option | Description
|
|
138
|
-
| ------------------- |
|
|
139
|
-
| `-p, --port <port>` | Port to listen on
|
|
140
|
-
| `-h, --host <host>` | Host to bind to
|
|
168
|
+
| Option | Description | Default |
|
|
169
|
+
| ------------------- | ------------------------ | --------- |
|
|
170
|
+
| `-p, --port <port>` | Port to listen on | `3001` |
|
|
171
|
+
| `-h, --host <host>` | Host to bind to | `0.0.0.0` |
|
|
172
|
+
| `-v, --verbose` | Show raw request details | |
|
|
173
|
+
| `--debug` | Alias for `--verbose` | |
|
|
141
174
|
|
|
142
175
|
**Features:**
|
|
143
176
|
|
|
@@ -145,11 +178,15 @@ better-webhook capture [options]
|
|
|
145
178
|
- Saves full request including headers, body, query params
|
|
146
179
|
- WebSocket server for real-time notifications
|
|
147
180
|
- Returns capture ID in response for easy reference
|
|
181
|
+
- `--verbose/--debug` prints raw request data; use with care since it may include sensitive payloads
|
|
148
182
|
|
|
149
183
|
**Example:**
|
|
150
184
|
|
|
151
185
|
```bash
|
|
152
186
|
better-webhook capture --port 4000 --host localhost
|
|
187
|
+
|
|
188
|
+
# Show request headers + raw body
|
|
189
|
+
better-webhook capture --verbose
|
|
153
190
|
```
|
|
154
191
|
|
|
155
192
|
---
|
package/dist/index.cjs
CHANGED
|
@@ -1297,11 +1297,13 @@ var CaptureServer = class {
|
|
|
1297
1297
|
captureCount = 0;
|
|
1298
1298
|
enableWebSocket;
|
|
1299
1299
|
onCapture;
|
|
1300
|
+
verbose;
|
|
1300
1301
|
constructor(options) {
|
|
1301
1302
|
const capturesDir = typeof options === "string" ? options : options?.capturesDir;
|
|
1302
1303
|
this.capturesDir = capturesDir || (0, import_path2.join)((0, import_os2.homedir)(), ".better-webhook", "captures");
|
|
1303
1304
|
this.enableWebSocket = typeof options === "object" ? options?.enableWebSocket !== false : true;
|
|
1304
1305
|
this.onCapture = typeof options === "object" ? options?.onCapture : void 0;
|
|
1306
|
+
this.verbose = typeof options === "object" ? options?.verbose === true : false;
|
|
1305
1307
|
if (!(0, import_fs2.existsSync)(this.capturesDir)) {
|
|
1306
1308
|
(0, import_fs2.mkdirSync)(this.capturesDir, { recursive: true });
|
|
1307
1309
|
}
|
|
@@ -1399,7 +1401,16 @@ var CaptureServer = class {
|
|
|
1399
1401
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1400
1402
|
const id = (0, import_crypto2.randomUUID)();
|
|
1401
1403
|
const url = req.url || "/";
|
|
1402
|
-
const
|
|
1404
|
+
const hostHeader = req.headers.host;
|
|
1405
|
+
const hostValue = typeof hostHeader === "string" ? hostHeader : "";
|
|
1406
|
+
const isHostSafe = /^[a-z0-9.-]+(:\d+)?$/i.test(hostValue);
|
|
1407
|
+
const baseUrl = isHostSafe ? `http://${hostValue}` : "http://localhost";
|
|
1408
|
+
let urlParts;
|
|
1409
|
+
try {
|
|
1410
|
+
urlParts = new URL(url, baseUrl);
|
|
1411
|
+
} catch {
|
|
1412
|
+
urlParts = new URL(url, "http://localhost");
|
|
1413
|
+
}
|
|
1403
1414
|
const query = {};
|
|
1404
1415
|
for (const [key, value] of urlParts.searchParams.entries()) {
|
|
1405
1416
|
if (query[key]) {
|
|
@@ -1459,6 +1470,34 @@ var CaptureServer = class {
|
|
|
1459
1470
|
console.log(
|
|
1460
1471
|
`\u{1F4E6} ${req.method} ${urlParts.pathname}${providerStr} -> ${filename}`
|
|
1461
1472
|
);
|
|
1473
|
+
if (this.verbose) {
|
|
1474
|
+
const headerEntries = Object.entries(req.headers).map(([key, value]) => {
|
|
1475
|
+
if (Array.isArray(value)) {
|
|
1476
|
+
return `${key}: ${value.join(", ")}`;
|
|
1477
|
+
}
|
|
1478
|
+
if (value === void 0) {
|
|
1479
|
+
return `${key}:`;
|
|
1480
|
+
}
|
|
1481
|
+
return `${key}: ${value}`;
|
|
1482
|
+
}).join("\n");
|
|
1483
|
+
const method = req.method || "GET";
|
|
1484
|
+
const providerLabel = provider || "unknown";
|
|
1485
|
+
const contentTypeLabel = contentType || "(none)";
|
|
1486
|
+
console.log(
|
|
1487
|
+
[
|
|
1488
|
+
"[debug] request",
|
|
1489
|
+
`method: ${method}`,
|
|
1490
|
+
`path: ${urlParts.pathname}`,
|
|
1491
|
+
`provider: ${providerLabel}`,
|
|
1492
|
+
`content-type: ${contentTypeLabel}`,
|
|
1493
|
+
`body-length: ${rawBody.length}`,
|
|
1494
|
+
"headers:",
|
|
1495
|
+
headerEntries || "(none)",
|
|
1496
|
+
"raw-body:",
|
|
1497
|
+
rawBody
|
|
1498
|
+
].join("\n")
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1462
1501
|
this.onCapture?.({ file: filename, capture: captured });
|
|
1463
1502
|
if (this.enableWebSocket) {
|
|
1464
1503
|
this.broadcast({
|
|
@@ -1594,27 +1633,30 @@ var CaptureServer = class {
|
|
|
1594
1633
|
};
|
|
1595
1634
|
|
|
1596
1635
|
// src/commands/capture.ts
|
|
1597
|
-
var capture = new import_commander3.Command().name("capture").description("Start a server to capture incoming webhooks").option("-p, --port <port>", "Port to listen on", "3001").option("-h, --host <host>", "Host to bind to", "0.0.0.0").
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
await server.
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1636
|
+
var capture = new import_commander3.Command().name("capture").description("Start a server to capture incoming webhooks").option("-p, --port <port>", "Port to listen on", "3001").option("-h, --host <host>", "Host to bind to", "0.0.0.0").option("-v, --verbose", "Show raw request details").option("--debug", "Alias for --verbose").action(
|
|
1637
|
+
async (options) => {
|
|
1638
|
+
const port = parseInt(options.port, 10);
|
|
1639
|
+
if (isNaN(port) || port < 0 || port > 65535) {
|
|
1640
|
+
console.error(import_chalk3.default.red("Invalid port number"));
|
|
1641
|
+
process.exitCode = 1;
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const verbose = Boolean(options.verbose || options.debug);
|
|
1645
|
+
const server = new CaptureServer({ verbose });
|
|
1646
|
+
try {
|
|
1647
|
+
await server.start(port, options.host);
|
|
1648
|
+
const shutdown = async () => {
|
|
1649
|
+
await server.stop();
|
|
1650
|
+
process.exit(0);
|
|
1651
|
+
};
|
|
1652
|
+
process.on("SIGINT", shutdown);
|
|
1653
|
+
process.on("SIGTERM", shutdown);
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
console.error(import_chalk3.default.red(`Failed to start server: ${error.message}`));
|
|
1656
|
+
process.exitCode = 1;
|
|
1657
|
+
}
|
|
1616
1658
|
}
|
|
1617
|
-
|
|
1659
|
+
);
|
|
1618
1660
|
|
|
1619
1661
|
// src/commands/captures.ts
|
|
1620
1662
|
var import_commander4 = require("commander");
|
|
@@ -2738,6 +2780,89 @@ function createDashboardApiRouter(options = {}) {
|
|
|
2738
2780
|
|
|
2739
2781
|
// src/core/dashboard-server.ts
|
|
2740
2782
|
var import_meta = {};
|
|
2783
|
+
function isStandaloneBinary() {
|
|
2784
|
+
if (typeof STANDALONE_BINARY !== "undefined" && STANDALONE_BINARY) {
|
|
2785
|
+
return true;
|
|
2786
|
+
}
|
|
2787
|
+
if (typeof globalThis.embeddedDashboardFiles !== "undefined" && globalThis.embeddedDashboardFiles) {
|
|
2788
|
+
return true;
|
|
2789
|
+
}
|
|
2790
|
+
return false;
|
|
2791
|
+
}
|
|
2792
|
+
function getMimeType(filePath) {
|
|
2793
|
+
const ext = import_path4.default.extname(filePath).toLowerCase();
|
|
2794
|
+
const mimeTypes = {
|
|
2795
|
+
".html": "text/html; charset=utf-8",
|
|
2796
|
+
".js": "application/javascript; charset=utf-8",
|
|
2797
|
+
".css": "text/css; charset=utf-8",
|
|
2798
|
+
".json": "application/json; charset=utf-8",
|
|
2799
|
+
".png": "image/png",
|
|
2800
|
+
".jpg": "image/jpeg",
|
|
2801
|
+
".jpeg": "image/jpeg",
|
|
2802
|
+
".gif": "image/gif",
|
|
2803
|
+
".svg": "image/svg+xml",
|
|
2804
|
+
".ico": "image/x-icon",
|
|
2805
|
+
".woff": "font/woff",
|
|
2806
|
+
".woff2": "font/woff2",
|
|
2807
|
+
".ttf": "font/ttf",
|
|
2808
|
+
".eot": "application/vnd.ms-fontobject"
|
|
2809
|
+
};
|
|
2810
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
2811
|
+
}
|
|
2812
|
+
function createEmbeddedDashboardMiddleware() {
|
|
2813
|
+
const filePathMap = /* @__PURE__ */ new Map();
|
|
2814
|
+
let indexHtmlPath = null;
|
|
2815
|
+
if (typeof globalThis.embeddedDashboardFiles !== "undefined") {
|
|
2816
|
+
for (const [key, filePath] of Object.entries(
|
|
2817
|
+
globalThis.embeddedDashboardFiles
|
|
2818
|
+
)) {
|
|
2819
|
+
const servePath = "/" + key.replace(/^dashboard\//, "");
|
|
2820
|
+
filePathMap.set(servePath, filePath);
|
|
2821
|
+
if (servePath === "/index.html") {
|
|
2822
|
+
indexHtmlPath = filePath;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const staticMiddleware = async (req, res, next) => {
|
|
2827
|
+
if (!Bun) {
|
|
2828
|
+
return next();
|
|
2829
|
+
}
|
|
2830
|
+
const requestPath = req.path === "/" ? "/index.html" : req.path;
|
|
2831
|
+
const filePath = filePathMap.get(requestPath);
|
|
2832
|
+
if (filePath) {
|
|
2833
|
+
try {
|
|
2834
|
+
const file = Bun.file(filePath);
|
|
2835
|
+
const content = await file.arrayBuffer();
|
|
2836
|
+
res.setHeader("Content-Type", getMimeType(requestPath));
|
|
2837
|
+
res.setHeader("Content-Length", content.byteLength);
|
|
2838
|
+
res.send(Buffer.from(content));
|
|
2839
|
+
} catch (err) {
|
|
2840
|
+
console.error(`Failed to serve embedded file ${requestPath}:`, err);
|
|
2841
|
+
next();
|
|
2842
|
+
}
|
|
2843
|
+
} else {
|
|
2844
|
+
next();
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const spaFallback = async (req, res, next) => {
|
|
2848
|
+
if (req.path.startsWith("/api") || req.path === "/health") {
|
|
2849
|
+
return next();
|
|
2850
|
+
}
|
|
2851
|
+
if (!Bun || !indexHtmlPath) {
|
|
2852
|
+
return next();
|
|
2853
|
+
}
|
|
2854
|
+
try {
|
|
2855
|
+
const file = Bun.file(indexHtmlPath);
|
|
2856
|
+
const content = await file.arrayBuffer();
|
|
2857
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2858
|
+
res.setHeader("Content-Length", content.byteLength);
|
|
2859
|
+
res.send(Buffer.from(content));
|
|
2860
|
+
} catch {
|
|
2861
|
+
next();
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
return { staticMiddleware, spaFallback };
|
|
2865
|
+
}
|
|
2741
2866
|
function resolveDashboardDistDir(runtimeDir) {
|
|
2742
2867
|
const candidates = [
|
|
2743
2868
|
// Bundled CLI: dist/index.js -> dist/dashboard
|
|
@@ -2791,18 +2916,24 @@ async function startDashboardServer(options = {}) {
|
|
|
2791
2916
|
);
|
|
2792
2917
|
const host = options.host || "localhost";
|
|
2793
2918
|
const port = options.port ?? 4e3;
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2919
|
+
if (isStandaloneBinary()) {
|
|
2920
|
+
const { staticMiddleware, spaFallback } = createEmbeddedDashboardMiddleware();
|
|
2921
|
+
app.use(staticMiddleware);
|
|
2922
|
+
app.get("*", spaFallback);
|
|
2923
|
+
} else {
|
|
2924
|
+
const runtimeDir = typeof __dirname !== "undefined" ? (
|
|
2925
|
+
// eslint-disable-next-line no-undef
|
|
2926
|
+
__dirname
|
|
2927
|
+
) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
2928
|
+
const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
|
|
2929
|
+
app.use(import_express2.default.static(dashboardDistDir));
|
|
2930
|
+
app.get("*", (req, res, next) => {
|
|
2931
|
+
if (req.path.startsWith("/api") || req.path === "/health") return next();
|
|
2932
|
+
res.sendFile(dashboardIndexHtml, (err) => {
|
|
2933
|
+
if (err) next();
|
|
2934
|
+
});
|
|
2804
2935
|
});
|
|
2805
|
-
}
|
|
2936
|
+
}
|
|
2806
2937
|
const server = (0, import_http2.createServer)(app);
|
|
2807
2938
|
const wss = new import_ws2.WebSocketServer({ server, path: "/ws" });
|
|
2808
2939
|
wss.on("connection", async (ws) => {
|
|
@@ -2929,14 +3060,24 @@ var dashboard = new import_commander6.Command().name("dashboard").description("S
|
|
|
2929
3060
|
|
|
2930
3061
|
// src/index.ts
|
|
2931
3062
|
var import_meta2 = {};
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
)
|
|
3063
|
+
function getVersion() {
|
|
3064
|
+
if (typeof CLI_VERSION !== "undefined") {
|
|
3065
|
+
return CLI_VERSION;
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
const packageJsonPath = (0, import_node_url.fileURLToPath)(
|
|
3069
|
+
new URL("../package.json", import_meta2.url)
|
|
3070
|
+
);
|
|
3071
|
+
const packageJson = JSON.parse(
|
|
3072
|
+
(0, import_node_fs.readFileSync)(packageJsonPath, { encoding: "utf8" })
|
|
3073
|
+
);
|
|
3074
|
+
return packageJson.version;
|
|
3075
|
+
} catch {
|
|
3076
|
+
return "0.0.0-unknown";
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
2938
3079
|
var program = new import_commander7.Command().name("better-webhook").description(
|
|
2939
3080
|
"Modern CLI for developing, capturing, and replaying webhooks locally"
|
|
2940
|
-
).version(
|
|
3081
|
+
).version(getVersion());
|
|
2941
3082
|
program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
|
|
2942
3083
|
program.parseAsync(process.argv);
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command as Command7 } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/commands/templates.ts
|
|
@@ -1291,11 +1291,13 @@ var CaptureServer = class {
|
|
|
1291
1291
|
captureCount = 0;
|
|
1292
1292
|
enableWebSocket;
|
|
1293
1293
|
onCapture;
|
|
1294
|
+
verbose;
|
|
1294
1295
|
constructor(options) {
|
|
1295
1296
|
const capturesDir = typeof options === "string" ? options : options?.capturesDir;
|
|
1296
1297
|
this.capturesDir = capturesDir || join2(homedir2(), ".better-webhook", "captures");
|
|
1297
1298
|
this.enableWebSocket = typeof options === "object" ? options?.enableWebSocket !== false : true;
|
|
1298
1299
|
this.onCapture = typeof options === "object" ? options?.onCapture : void 0;
|
|
1300
|
+
this.verbose = typeof options === "object" ? options?.verbose === true : false;
|
|
1299
1301
|
if (!existsSync2(this.capturesDir)) {
|
|
1300
1302
|
mkdirSync2(this.capturesDir, { recursive: true });
|
|
1301
1303
|
}
|
|
@@ -1393,7 +1395,16 @@ var CaptureServer = class {
|
|
|
1393
1395
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1394
1396
|
const id = randomUUID();
|
|
1395
1397
|
const url = req.url || "/";
|
|
1396
|
-
const
|
|
1398
|
+
const hostHeader = req.headers.host;
|
|
1399
|
+
const hostValue = typeof hostHeader === "string" ? hostHeader : "";
|
|
1400
|
+
const isHostSafe = /^[a-z0-9.-]+(:\d+)?$/i.test(hostValue);
|
|
1401
|
+
const baseUrl = isHostSafe ? `http://${hostValue}` : "http://localhost";
|
|
1402
|
+
let urlParts;
|
|
1403
|
+
try {
|
|
1404
|
+
urlParts = new URL(url, baseUrl);
|
|
1405
|
+
} catch {
|
|
1406
|
+
urlParts = new URL(url, "http://localhost");
|
|
1407
|
+
}
|
|
1397
1408
|
const query = {};
|
|
1398
1409
|
for (const [key, value] of urlParts.searchParams.entries()) {
|
|
1399
1410
|
if (query[key]) {
|
|
@@ -1453,6 +1464,34 @@ var CaptureServer = class {
|
|
|
1453
1464
|
console.log(
|
|
1454
1465
|
`\u{1F4E6} ${req.method} ${urlParts.pathname}${providerStr} -> ${filename}`
|
|
1455
1466
|
);
|
|
1467
|
+
if (this.verbose) {
|
|
1468
|
+
const headerEntries = Object.entries(req.headers).map(([key, value]) => {
|
|
1469
|
+
if (Array.isArray(value)) {
|
|
1470
|
+
return `${key}: ${value.join(", ")}`;
|
|
1471
|
+
}
|
|
1472
|
+
if (value === void 0) {
|
|
1473
|
+
return `${key}:`;
|
|
1474
|
+
}
|
|
1475
|
+
return `${key}: ${value}`;
|
|
1476
|
+
}).join("\n");
|
|
1477
|
+
const method = req.method || "GET";
|
|
1478
|
+
const providerLabel = provider || "unknown";
|
|
1479
|
+
const contentTypeLabel = contentType || "(none)";
|
|
1480
|
+
console.log(
|
|
1481
|
+
[
|
|
1482
|
+
"[debug] request",
|
|
1483
|
+
`method: ${method}`,
|
|
1484
|
+
`path: ${urlParts.pathname}`,
|
|
1485
|
+
`provider: ${providerLabel}`,
|
|
1486
|
+
`content-type: ${contentTypeLabel}`,
|
|
1487
|
+
`body-length: ${rawBody.length}`,
|
|
1488
|
+
"headers:",
|
|
1489
|
+
headerEntries || "(none)",
|
|
1490
|
+
"raw-body:",
|
|
1491
|
+
rawBody
|
|
1492
|
+
].join("\n")
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1456
1495
|
this.onCapture?.({ file: filename, capture: captured });
|
|
1457
1496
|
if (this.enableWebSocket) {
|
|
1458
1497
|
this.broadcast({
|
|
@@ -1588,27 +1627,30 @@ var CaptureServer = class {
|
|
|
1588
1627
|
};
|
|
1589
1628
|
|
|
1590
1629
|
// src/commands/capture.ts
|
|
1591
|
-
var capture = new Command3().name("capture").description("Start a server to capture incoming webhooks").option("-p, --port <port>", "Port to listen on", "3001").option("-h, --host <host>", "Host to bind to", "0.0.0.0").
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
await server.
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1630
|
+
var capture = new Command3().name("capture").description("Start a server to capture incoming webhooks").option("-p, --port <port>", "Port to listen on", "3001").option("-h, --host <host>", "Host to bind to", "0.0.0.0").option("-v, --verbose", "Show raw request details").option("--debug", "Alias for --verbose").action(
|
|
1631
|
+
async (options) => {
|
|
1632
|
+
const port = parseInt(options.port, 10);
|
|
1633
|
+
if (isNaN(port) || port < 0 || port > 65535) {
|
|
1634
|
+
console.error(chalk3.red("Invalid port number"));
|
|
1635
|
+
process.exitCode = 1;
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const verbose = Boolean(options.verbose || options.debug);
|
|
1639
|
+
const server = new CaptureServer({ verbose });
|
|
1640
|
+
try {
|
|
1641
|
+
await server.start(port, options.host);
|
|
1642
|
+
const shutdown = async () => {
|
|
1643
|
+
await server.stop();
|
|
1644
|
+
process.exit(0);
|
|
1645
|
+
};
|
|
1646
|
+
process.on("SIGINT", shutdown);
|
|
1647
|
+
process.on("SIGTERM", shutdown);
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
console.error(chalk3.red(`Failed to start server: ${error.message}`));
|
|
1650
|
+
process.exitCode = 1;
|
|
1651
|
+
}
|
|
1610
1652
|
}
|
|
1611
|
-
|
|
1653
|
+
);
|
|
1612
1654
|
|
|
1613
1655
|
// src/commands/captures.ts
|
|
1614
1656
|
import { Command as Command4 } from "commander";
|
|
@@ -2731,6 +2773,89 @@ function createDashboardApiRouter(options = {}) {
|
|
|
2731
2773
|
}
|
|
2732
2774
|
|
|
2733
2775
|
// src/core/dashboard-server.ts
|
|
2776
|
+
function isStandaloneBinary() {
|
|
2777
|
+
if (typeof STANDALONE_BINARY !== "undefined" && STANDALONE_BINARY) {
|
|
2778
|
+
return true;
|
|
2779
|
+
}
|
|
2780
|
+
if (typeof globalThis.embeddedDashboardFiles !== "undefined" && globalThis.embeddedDashboardFiles) {
|
|
2781
|
+
return true;
|
|
2782
|
+
}
|
|
2783
|
+
return false;
|
|
2784
|
+
}
|
|
2785
|
+
function getMimeType(filePath) {
|
|
2786
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2787
|
+
const mimeTypes = {
|
|
2788
|
+
".html": "text/html; charset=utf-8",
|
|
2789
|
+
".js": "application/javascript; charset=utf-8",
|
|
2790
|
+
".css": "text/css; charset=utf-8",
|
|
2791
|
+
".json": "application/json; charset=utf-8",
|
|
2792
|
+
".png": "image/png",
|
|
2793
|
+
".jpg": "image/jpeg",
|
|
2794
|
+
".jpeg": "image/jpeg",
|
|
2795
|
+
".gif": "image/gif",
|
|
2796
|
+
".svg": "image/svg+xml",
|
|
2797
|
+
".ico": "image/x-icon",
|
|
2798
|
+
".woff": "font/woff",
|
|
2799
|
+
".woff2": "font/woff2",
|
|
2800
|
+
".ttf": "font/ttf",
|
|
2801
|
+
".eot": "application/vnd.ms-fontobject"
|
|
2802
|
+
};
|
|
2803
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
2804
|
+
}
|
|
2805
|
+
function createEmbeddedDashboardMiddleware() {
|
|
2806
|
+
const filePathMap = /* @__PURE__ */ new Map();
|
|
2807
|
+
let indexHtmlPath = null;
|
|
2808
|
+
if (typeof globalThis.embeddedDashboardFiles !== "undefined") {
|
|
2809
|
+
for (const [key, filePath] of Object.entries(
|
|
2810
|
+
globalThis.embeddedDashboardFiles
|
|
2811
|
+
)) {
|
|
2812
|
+
const servePath = "/" + key.replace(/^dashboard\//, "");
|
|
2813
|
+
filePathMap.set(servePath, filePath);
|
|
2814
|
+
if (servePath === "/index.html") {
|
|
2815
|
+
indexHtmlPath = filePath;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
const staticMiddleware = async (req, res, next) => {
|
|
2820
|
+
if (!Bun) {
|
|
2821
|
+
return next();
|
|
2822
|
+
}
|
|
2823
|
+
const requestPath = req.path === "/" ? "/index.html" : req.path;
|
|
2824
|
+
const filePath = filePathMap.get(requestPath);
|
|
2825
|
+
if (filePath) {
|
|
2826
|
+
try {
|
|
2827
|
+
const file = Bun.file(filePath);
|
|
2828
|
+
const content = await file.arrayBuffer();
|
|
2829
|
+
res.setHeader("Content-Type", getMimeType(requestPath));
|
|
2830
|
+
res.setHeader("Content-Length", content.byteLength);
|
|
2831
|
+
res.send(Buffer.from(content));
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
console.error(`Failed to serve embedded file ${requestPath}:`, err);
|
|
2834
|
+
next();
|
|
2835
|
+
}
|
|
2836
|
+
} else {
|
|
2837
|
+
next();
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
const spaFallback = async (req, res, next) => {
|
|
2841
|
+
if (req.path.startsWith("/api") || req.path === "/health") {
|
|
2842
|
+
return next();
|
|
2843
|
+
}
|
|
2844
|
+
if (!Bun || !indexHtmlPath) {
|
|
2845
|
+
return next();
|
|
2846
|
+
}
|
|
2847
|
+
try {
|
|
2848
|
+
const file = Bun.file(indexHtmlPath);
|
|
2849
|
+
const content = await file.arrayBuffer();
|
|
2850
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2851
|
+
res.setHeader("Content-Length", content.byteLength);
|
|
2852
|
+
res.send(Buffer.from(content));
|
|
2853
|
+
} catch {
|
|
2854
|
+
next();
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
return { staticMiddleware, spaFallback };
|
|
2858
|
+
}
|
|
2734
2859
|
function resolveDashboardDistDir(runtimeDir) {
|
|
2735
2860
|
const candidates = [
|
|
2736
2861
|
// Bundled CLI: dist/index.js -> dist/dashboard
|
|
@@ -2784,18 +2909,24 @@ async function startDashboardServer(options = {}) {
|
|
|
2784
2909
|
);
|
|
2785
2910
|
const host = options.host || "localhost";
|
|
2786
2911
|
const port = options.port ?? 4e3;
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2912
|
+
if (isStandaloneBinary()) {
|
|
2913
|
+
const { staticMiddleware, spaFallback } = createEmbeddedDashboardMiddleware();
|
|
2914
|
+
app.use(staticMiddleware);
|
|
2915
|
+
app.get("*", spaFallback);
|
|
2916
|
+
} else {
|
|
2917
|
+
const runtimeDir = typeof __dirname !== "undefined" ? (
|
|
2918
|
+
// eslint-disable-next-line no-undef
|
|
2919
|
+
__dirname
|
|
2920
|
+
) : path.dirname(fileURLToPath(import.meta.url));
|
|
2921
|
+
const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
|
|
2922
|
+
app.use(express2.static(dashboardDistDir));
|
|
2923
|
+
app.get("*", (req, res, next) => {
|
|
2924
|
+
if (req.path.startsWith("/api") || req.path === "/health") return next();
|
|
2925
|
+
res.sendFile(dashboardIndexHtml, (err) => {
|
|
2926
|
+
if (err) next();
|
|
2927
|
+
});
|
|
2797
2928
|
});
|
|
2798
|
-
}
|
|
2929
|
+
}
|
|
2799
2930
|
const server = createServer2(app);
|
|
2800
2931
|
const wss = new WebSocketServer2({ server, path: "/ws" });
|
|
2801
2932
|
wss.on("connection", async (ws) => {
|
|
@@ -2921,14 +3052,24 @@ var dashboard = new Command6().name("dashboard").description("Start the local da
|
|
|
2921
3052
|
});
|
|
2922
3053
|
|
|
2923
3054
|
// src/index.ts
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3055
|
+
function getVersion() {
|
|
3056
|
+
if (typeof CLI_VERSION !== "undefined") {
|
|
3057
|
+
return CLI_VERSION;
|
|
3058
|
+
}
|
|
3059
|
+
try {
|
|
3060
|
+
const packageJsonPath = fileURLToPath2(
|
|
3061
|
+
new URL("../package.json", import.meta.url)
|
|
3062
|
+
);
|
|
3063
|
+
const packageJson = JSON.parse(
|
|
3064
|
+
readFileSync5(packageJsonPath, { encoding: "utf8" })
|
|
3065
|
+
);
|
|
3066
|
+
return packageJson.version;
|
|
3067
|
+
} catch {
|
|
3068
|
+
return "0.0.0-unknown";
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
2930
3071
|
var program = new Command7().name("better-webhook").description(
|
|
2931
3072
|
"Modern CLI for developing, capturing, and replaying webhooks locally"
|
|
2932
|
-
).version(
|
|
3073
|
+
).version(getVersion());
|
|
2933
3074
|
program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
|
|
2934
3075
|
program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-webhook/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Modern CLI for developing, capturing, and replaying webhooks locally with dashboard UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"zod": "^3.23.8"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@types/bun": "^1.2.18",
|
|
63
64
|
"@types/express": "^4.17.21",
|
|
64
65
|
"@types/node": "^24.3.1",
|
|
65
66
|
"@types/prompts": "^2.4.9",
|
|
@@ -72,6 +73,8 @@
|
|
|
72
73
|
"scripts": {
|
|
73
74
|
"build:cli": "tsup --format cjs,esm --dts && node ./scripts/copy-dashboard.mjs",
|
|
74
75
|
"build": "pnpm --filter @better-webhook/dashboard build && pnpm run build:cli",
|
|
76
|
+
"build:binary": "bun ./scripts/build-binary.ts",
|
|
77
|
+
"build:binary:all": "bun ./scripts/build-binary.ts --all",
|
|
75
78
|
"dev": "tsup --watch",
|
|
76
79
|
"start": "tsx src/index.ts",
|
|
77
80
|
"lint": "tsc",
|