@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 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 | Default |
138
- | ------------------- | ----------------- | --------- |
139
- | `-p, --port <port>` | Port to listen on | `3001` |
140
- | `-h, --host <host>` | Host to bind to | `0.0.0.0` |
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 urlParts = new URL(url, `http://${req.headers.host || "localhost"}`);
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").action(async (options) => {
1598
- const port = parseInt(options.port, 10);
1599
- if (isNaN(port) || port < 0 || port > 65535) {
1600
- console.error(import_chalk3.default.red("Invalid port number"));
1601
- process.exitCode = 1;
1602
- return;
1603
- }
1604
- const server = new CaptureServer();
1605
- try {
1606
- await server.start(port, options.host);
1607
- const shutdown = async () => {
1608
- await server.stop();
1609
- process.exit(0);
1610
- };
1611
- process.on("SIGINT", shutdown);
1612
- process.on("SIGTERM", shutdown);
1613
- } catch (error) {
1614
- console.error(import_chalk3.default.red(`Failed to start server: ${error.message}`));
1615
- process.exitCode = 1;
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
- const runtimeDir = typeof __dirname !== "undefined" ? (
2795
- // eslint-disable-next-line no-undef
2796
- __dirname
2797
- ) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url));
2798
- const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2799
- app.use(import_express2.default.static(dashboardDistDir));
2800
- app.get("*", (req, res, next) => {
2801
- if (req.path.startsWith("/api") || req.path === "/health") return next();
2802
- res.sendFile(dashboardIndexHtml, (err) => {
2803
- if (err) next();
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
- var packageJsonPath = (0, import_node_url.fileURLToPath)(
2933
- new URL("../package.json", import_meta2.url)
2934
- );
2935
- var packageJson = JSON.parse(
2936
- (0, import_node_fs.readFileSync)(packageJsonPath, { encoding: "utf8" })
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(packageJson.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 readFileSync4 } from "fs";
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 urlParts = new URL(url, `http://${req.headers.host || "localhost"}`);
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").action(async (options) => {
1592
- const port = parseInt(options.port, 10);
1593
- if (isNaN(port) || port < 0 || port > 65535) {
1594
- console.error(chalk3.red("Invalid port number"));
1595
- process.exitCode = 1;
1596
- return;
1597
- }
1598
- const server = new CaptureServer();
1599
- try {
1600
- await server.start(port, options.host);
1601
- const shutdown = async () => {
1602
- await server.stop();
1603
- process.exit(0);
1604
- };
1605
- process.on("SIGINT", shutdown);
1606
- process.on("SIGTERM", shutdown);
1607
- } catch (error) {
1608
- console.error(chalk3.red(`Failed to start server: ${error.message}`));
1609
- process.exitCode = 1;
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
- const runtimeDir = typeof __dirname !== "undefined" ? (
2788
- // eslint-disable-next-line no-undef
2789
- __dirname
2790
- ) : path.dirname(fileURLToPath(import.meta.url));
2791
- const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2792
- app.use(express2.static(dashboardDistDir));
2793
- app.get("*", (req, res, next) => {
2794
- if (req.path.startsWith("/api") || req.path === "/health") return next();
2795
- res.sendFile(dashboardIndexHtml, (err) => {
2796
- if (err) next();
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
- var packageJsonPath = fileURLToPath2(
2925
- new URL("../package.json", import.meta.url)
2926
- );
2927
- var packageJson = JSON.parse(
2928
- readFileSync4(packageJsonPath, { encoding: "utf8" })
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(packageJson.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.5.0",
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",