@better-webhook/cli 3.4.4 → 3.6.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 (41) hide show
  1. package/README.md +66 -0
  2. package/dist/dashboard/assets/index-CxrRCNTh.css +1 -0
  3. package/dist/dashboard/assets/index-Dlqdzwyc.js +23 -0
  4. package/dist/dashboard/index.html +2 -2
  5. package/dist/index.cjs +447 -33
  6. package/dist/index.js +448 -34
  7. package/package.json +7 -3
  8. package/dist/commands/capture.d.ts +0 -2
  9. package/dist/commands/capture.js +0 -30
  10. package/dist/commands/captures.d.ts +0 -2
  11. package/dist/commands/captures.js +0 -217
  12. package/dist/commands/dashboard.d.ts +0 -2
  13. package/dist/commands/dashboard.js +0 -65
  14. package/dist/commands/index.d.ts +0 -6
  15. package/dist/commands/index.js +0 -6
  16. package/dist/commands/replay.d.ts +0 -2
  17. package/dist/commands/replay.js +0 -140
  18. package/dist/commands/run.d.ts +0 -2
  19. package/dist/commands/run.js +0 -181
  20. package/dist/commands/templates.d.ts +0 -2
  21. package/dist/commands/templates.js +0 -285
  22. package/dist/core/capture-server.d.ts +0 -31
  23. package/dist/core/capture-server.js +0 -298
  24. package/dist/core/dashboard-api.d.ts +0 -8
  25. package/dist/core/dashboard-api.js +0 -271
  26. package/dist/core/dashboard-server.d.ts +0 -20
  27. package/dist/core/dashboard-server.js +0 -124
  28. package/dist/core/executor.d.ts +0 -11
  29. package/dist/core/executor.js +0 -130
  30. package/dist/core/index.d.ts +0 -5
  31. package/dist/core/index.js +0 -5
  32. package/dist/core/replay-engine.d.ts +0 -18
  33. package/dist/core/replay-engine.js +0 -208
  34. package/dist/core/signature.d.ts +0 -24
  35. package/dist/core/signature.js +0 -199
  36. package/dist/core/template-manager.d.ts +0 -24
  37. package/dist/core/template-manager.js +0 -246
  38. package/dist/dashboard/assets/index-BSfTbn4Y.js +0 -23
  39. package/dist/dashboard/assets/index-zDTVdss_.css +0 -1
  40. package/dist/types/index.d.ts +0 -299
  41. package/dist/types/index.js +0 -86
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>better-webhook dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-BSfTbn4Y.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-zDTVdss_.css">
8
+ <script type="module" crossorigin src="/assets/index-Dlqdzwyc.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CxrRCNTh.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/dist/index.cjs CHANGED
@@ -124,6 +124,12 @@ var ConfigSchema = import_zod.z.object({
124
124
  // src/core/template-manager.ts
125
125
  var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
126
126
  var TEMPLATES_INDEX_URL = `${GITHUB_RAW_BASE}/templates/templates.json`;
127
+ var TEMPLATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
128
+ function isValidTemplateId(id) {
129
+ if (!id || id.length > 128) return false;
130
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) return false;
131
+ return TEMPLATE_ID_PATTERN.test(id);
132
+ }
127
133
  var TemplateManager = class {
128
134
  baseDir;
129
135
  templatesDir;
@@ -377,6 +383,76 @@ var TemplateManager = class {
377
383
  }
378
384
  return deleted;
379
385
  }
386
+ /**
387
+ * Check if a template with the given ID already exists locally
388
+ */
389
+ templateExists(templateId) {
390
+ return this.getLocalTemplate(templateId) !== null;
391
+ }
392
+ /**
393
+ * Generate a unique template ID from provider and event
394
+ */
395
+ generateTemplateId(provider, event) {
396
+ const providerPart = provider || "custom";
397
+ const eventPart = event || "webhook";
398
+ const baseId = `${providerPart}-${eventPart}`.toLowerCase().replace(/\s+/g, "-");
399
+ if (!this.templateExists(baseId)) {
400
+ return baseId;
401
+ }
402
+ let counter = 1;
403
+ while (this.templateExists(`${baseId}-${counter}`)) {
404
+ counter++;
405
+ }
406
+ return `${baseId}-${counter}`;
407
+ }
408
+ /**
409
+ * Save a user-created template from a captured webhook
410
+ */
411
+ saveUserTemplate(template, options = {}) {
412
+ const provider = template.provider || "custom";
413
+ const event = options.event || template.event || "webhook";
414
+ const templateId = options.id || this.generateTemplateId(provider, event);
415
+ const name = options.name || templateId;
416
+ const description = options.description || template.description;
417
+ if (!isValidTemplateId(templateId)) {
418
+ throw new Error(
419
+ `Invalid template ID "${templateId}". IDs must start with alphanumeric, contain only letters, numbers, dots, underscores, and hyphens.`
420
+ );
421
+ }
422
+ if (!options.overwrite && this.templateExists(templateId)) {
423
+ throw new Error(
424
+ `Template with ID "${templateId}" already exists. Use --overwrite to replace it.`
425
+ );
426
+ }
427
+ const providerDir = (0, import_path.join)(this.templatesDir, provider);
428
+ if (!(0, import_fs.existsSync)(providerDir)) {
429
+ (0, import_fs.mkdirSync)(providerDir, { recursive: true });
430
+ }
431
+ const metadata = {
432
+ id: templateId,
433
+ name,
434
+ provider,
435
+ event,
436
+ file: `${provider}/${templateId}.json`,
437
+ description,
438
+ source: "capture",
439
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
440
+ };
441
+ const saveData = {
442
+ ...template,
443
+ provider,
444
+ event,
445
+ description,
446
+ _metadata: metadata
447
+ };
448
+ const filePath = (0, import_path.join)(providerDir, `${templateId}.json`);
449
+ (0, import_fs.writeFileSync)(filePath, JSON.stringify(saveData, null, 2));
450
+ return {
451
+ id: templateId,
452
+ filePath,
453
+ template: saveData
454
+ };
455
+ }
380
456
  };
381
457
  var instance = null;
382
458
  function getTemplateManager(baseDir) {
@@ -1544,6 +1620,7 @@ var capture = new import_commander3.Command().name("capture").description("Start
1544
1620
  var import_commander4 = require("commander");
1545
1621
  var import_chalk4 = __toESM(require("chalk"), 1);
1546
1622
  var import_prompts3 = __toESM(require("prompts"), 1);
1623
+ var import_os4 = require("os");
1547
1624
 
1548
1625
  // src/core/replay-engine.ts
1549
1626
  var import_fs3 = require("fs");
@@ -1677,15 +1754,93 @@ var ReplayEngine = class {
1677
1754
  body = capture2.rawBody;
1678
1755
  }
1679
1756
  }
1757
+ const event = options?.event || this.detectEvent(capture2);
1680
1758
  return {
1681
1759
  url: options?.url || `http://localhost:3000${capture2.path}`,
1682
1760
  method: capture2.method,
1683
1761
  headers,
1684
1762
  body,
1685
1763
  provider: capture2.provider,
1764
+ event,
1686
1765
  description: `Captured ${capture2.provider || "webhook"} at ${capture2.timestamp}`
1687
1766
  };
1688
1767
  }
1768
+ /**
1769
+ * Detect event type from captured webhook headers/body
1770
+ */
1771
+ detectEvent(capture2) {
1772
+ const headers = capture2.headers;
1773
+ const githubEvent = headers["x-github-event"];
1774
+ if (githubEvent) {
1775
+ return Array.isArray(githubEvent) ? githubEvent[0] : githubEvent;
1776
+ }
1777
+ if (capture2.provider === "stripe" && capture2.body) {
1778
+ const body = capture2.body;
1779
+ if (typeof body.type === "string") {
1780
+ return body.type;
1781
+ }
1782
+ }
1783
+ if (capture2.provider === "slack" && capture2.body) {
1784
+ const body = capture2.body;
1785
+ if (typeof body.type === "string") {
1786
+ return body.type;
1787
+ }
1788
+ const event = body.event;
1789
+ if (event && typeof event.type === "string") {
1790
+ return event.type;
1791
+ }
1792
+ }
1793
+ if (capture2.provider === "linear" && capture2.body) {
1794
+ const body = capture2.body;
1795
+ if (typeof body.type === "string") {
1796
+ return body.type;
1797
+ }
1798
+ }
1799
+ if (capture2.provider === "clerk" && capture2.body) {
1800
+ const body = capture2.body;
1801
+ if (typeof body.type === "string") {
1802
+ return body.type;
1803
+ }
1804
+ }
1805
+ if (capture2.provider === "ragie" && capture2.body) {
1806
+ const body = capture2.body;
1807
+ if (typeof body.event_type === "string") {
1808
+ return body.event_type;
1809
+ }
1810
+ }
1811
+ const shopifyTopic = headers["x-shopify-topic"];
1812
+ if (shopifyTopic) {
1813
+ return Array.isArray(shopifyTopic) ? shopifyTopic[0] : shopifyTopic;
1814
+ }
1815
+ if (capture2.provider === "sendgrid" && Array.isArray(capture2.body)) {
1816
+ const firstEvent = capture2.body[0];
1817
+ if (firstEvent && typeof firstEvent.event === "string") {
1818
+ return firstEvent.event;
1819
+ }
1820
+ }
1821
+ if (capture2.provider === "discord" && capture2.body) {
1822
+ const body = capture2.body;
1823
+ if (typeof body.type === "number") {
1824
+ return `type_${body.type}`;
1825
+ }
1826
+ }
1827
+ if (capture2.body && typeof capture2.body === "object") {
1828
+ const body = capture2.body;
1829
+ if (typeof body.type === "string") {
1830
+ return body.type;
1831
+ }
1832
+ if (typeof body.event_type === "string") {
1833
+ return body.event_type;
1834
+ }
1835
+ if (typeof body.event === "string") {
1836
+ return body.event;
1837
+ }
1838
+ if (typeof body.action === "string") {
1839
+ return body.action;
1840
+ }
1841
+ }
1842
+ return void 0;
1843
+ }
1689
1844
  /**
1690
1845
  * Get a summary of a capture
1691
1846
  */
@@ -1774,6 +1929,13 @@ function getReplayEngine(capturesDir) {
1774
1929
  }
1775
1930
 
1776
1931
  // src/commands/captures.ts
1932
+ function toRelativePath(absolutePath) {
1933
+ const home = (0, import_os4.homedir)();
1934
+ if (absolutePath.startsWith(home)) {
1935
+ return "~" + absolutePath.slice(home.length);
1936
+ }
1937
+ return absolutePath;
1938
+ }
1777
1939
  var listCommand2 = new import_commander4.Command().name("list").alias("ls").description("List captured webhooks").option("-l, --limit <limit>", "Maximum number of captures to show", "20").option("-p, --provider <provider>", "Filter by provider").action((options) => {
1778
1940
  const limit = parseInt(options.limit, 10);
1779
1941
  if (isNaN(limit) || limit <= 0) {
@@ -1982,7 +2144,92 @@ var cleanCommand2 = new import_commander4.Command().name("clean").alias("remove-
1982
2144
  console.log(import_chalk4.default.gray(` Storage: ${engine.getCapturesDir()}
1983
2145
  `));
1984
2146
  });
1985
- var captures = new import_commander4.Command().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2);
2147
+ var saveAsTemplateCommand = new import_commander4.Command().name("save-as-template").alias("sat").argument("<captureId>", "Capture ID or partial ID").description("Save a captured webhook as a reusable template").option("--id <id>", "Template ID (auto-generated if not provided)").option("--name <name>", "Template display name").option("--event <event>", "Event type (auto-detected if not provided)").option("--description <description>", "Template description").option("--url <url>", "Default target URL for the template").option("--overwrite", "Overwrite existing template with same ID").action(
2148
+ async (captureId, options) => {
2149
+ const engine = getReplayEngine();
2150
+ const templateManager = getTemplateManager();
2151
+ const captureFile = engine.getCapture(captureId);
2152
+ if (!captureFile) {
2153
+ console.log(import_chalk4.default.red(`
2154
+ \u274C Capture not found: ${captureId}
2155
+ `));
2156
+ process.exitCode = 1;
2157
+ return;
2158
+ }
2159
+ const { capture: capture2 } = captureFile;
2160
+ console.log(import_chalk4.default.bold("\n\u{1F4CB} Capture to save as template:\n"));
2161
+ console.log(` ${import_chalk4.default.white(capture2.id.slice(0, 8))}`);
2162
+ console.log(import_chalk4.default.gray(` ${capture2.method} ${capture2.path}`));
2163
+ if (capture2.provider) {
2164
+ console.log(import_chalk4.default.gray(` Provider: ${capture2.provider}`));
2165
+ }
2166
+ console.log();
2167
+ const template = engine.captureToTemplate(captureId, {
2168
+ url: options.url,
2169
+ event: options.event
2170
+ });
2171
+ let templateId = options.id;
2172
+ if (!templateId) {
2173
+ const suggestedId = `${capture2.provider || "custom"}-${template.event || "webhook"}`.toLowerCase().replace(/\s+/g, "-");
2174
+ const response = await (0, import_prompts3.default)({
2175
+ type: "text",
2176
+ name: "templateId",
2177
+ message: "Template ID:",
2178
+ initial: suggestedId,
2179
+ validate: (value) => value.trim().length > 0 || "Template ID is required"
2180
+ });
2181
+ if (!response.templateId) {
2182
+ console.log(import_chalk4.default.yellow("Cancelled"));
2183
+ return;
2184
+ }
2185
+ templateId = response.templateId;
2186
+ }
2187
+ if (!options.overwrite && templateId && templateManager.templateExists(templateId)) {
2188
+ const response = await (0, import_prompts3.default)({
2189
+ type: "confirm",
2190
+ name: "overwrite",
2191
+ message: `Template "${templateId}" already exists. Overwrite?`,
2192
+ initial: false
2193
+ });
2194
+ if (!response.overwrite) {
2195
+ console.log(import_chalk4.default.yellow("Cancelled"));
2196
+ return;
2197
+ }
2198
+ options.overwrite = true;
2199
+ }
2200
+ try {
2201
+ const result = templateManager.saveUserTemplate(template, {
2202
+ id: templateId,
2203
+ name: options.name,
2204
+ event: options.event || template.event,
2205
+ description: options.description,
2206
+ overwrite: options.overwrite
2207
+ });
2208
+ console.log(import_chalk4.default.green(`
2209
+ \u2713 Saved template: ${result.id}`));
2210
+ console.log(import_chalk4.default.gray(` File: ${toRelativePath(result.filePath)}`));
2211
+ console.log(import_chalk4.default.gray(` Provider: ${template.provider || "custom"}`));
2212
+ if (template.event) {
2213
+ console.log(import_chalk4.default.gray(` Event: ${template.event}`));
2214
+ }
2215
+ console.log();
2216
+ console.log(import_chalk4.default.gray(" Run it with:"));
2217
+ console.log(
2218
+ import_chalk4.default.cyan(
2219
+ ` better-webhook run ${result.id} --url http://localhost:3000/webhooks
2220
+ `
2221
+ )
2222
+ );
2223
+ } catch (error) {
2224
+ const message = error instanceof Error ? error.message : "Failed to save template";
2225
+ console.log(import_chalk4.default.red(`
2226
+ \u274C ${message}
2227
+ `));
2228
+ process.exitCode = 1;
2229
+ }
2230
+ }
2231
+ );
2232
+ var captures = new import_commander4.Command().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2).addCommand(saveAsTemplateCommand);
1986
2233
 
1987
2234
  // src/commands/replay.ts
1988
2235
  var import_commander5 = require("commander");
@@ -2169,12 +2416,28 @@ var ReplayBodySchema = import_zod2.z.object({
2169
2416
  var TemplateDownloadBodySchema = import_zod2.z.object({
2170
2417
  id: import_zod2.z.string().min(1)
2171
2418
  });
2419
+ var TemplateIdSchema = import_zod2.z.string().regex(
2420
+ /^[a-z0-9][a-z0-9._-]*$/i,
2421
+ "ID must start with alphanumeric and contain only letters, numbers, dots, underscores, and hyphens"
2422
+ ).max(128, "ID must be 128 characters or less").refine(
2423
+ (val) => !val.includes("/") && !val.includes("\\") && !val.includes(".."),
2424
+ "ID cannot contain path separators or parent directory references"
2425
+ );
2172
2426
  var RunTemplateBodySchema = import_zod2.z.object({
2173
2427
  templateId: import_zod2.z.string().min(1),
2174
2428
  url: import_zod2.z.string().min(1),
2175
2429
  secret: import_zod2.z.string().optional(),
2176
2430
  headers: import_zod2.z.array(HeaderEntrySchema).optional()
2177
2431
  });
2432
+ var SaveAsTemplateBodySchema = import_zod2.z.object({
2433
+ captureId: import_zod2.z.string().min(1),
2434
+ id: TemplateIdSchema.optional(),
2435
+ name: import_zod2.z.string().optional(),
2436
+ event: import_zod2.z.string().optional(),
2437
+ description: import_zod2.z.string().optional(),
2438
+ url: import_zod2.z.string().optional(),
2439
+ overwrite: import_zod2.z.boolean().optional()
2440
+ });
2178
2441
  function createDashboardApiRouter(options = {}) {
2179
2442
  const router = import_express.default.Router();
2180
2443
  const replayEngine = new ReplayEngine(options.capturesDir);
@@ -2190,22 +2453,26 @@ function createDashboardApiRouter(options = {}) {
2190
2453
  };
2191
2454
  const broadcastTemplates = async () => {
2192
2455
  if (!broadcast) return;
2193
- const local = templateManager.listLocalTemplates();
2194
- let remote = [];
2195
2456
  try {
2196
- const index = await templateManager.fetchRemoteIndex(false);
2197
- const localIds = new Set(local.map((t) => t.id));
2198
- remote = index.templates.map((metadata) => ({
2199
- metadata,
2200
- isDownloaded: localIds.has(metadata.id)
2201
- }));
2202
- } catch {
2203
- remote = [];
2457
+ const local = templateManager.listLocalTemplates();
2458
+ let remote = [];
2459
+ try {
2460
+ const index = await templateManager.fetchRemoteIndex(false);
2461
+ const localIds = new Set(local.map((t) => t.id));
2462
+ remote = index.templates.map((metadata) => ({
2463
+ metadata,
2464
+ isDownloaded: localIds.has(metadata.id)
2465
+ }));
2466
+ } catch {
2467
+ remote = [];
2468
+ }
2469
+ broadcast({
2470
+ type: "templates_updated",
2471
+ payload: { local, remote }
2472
+ });
2473
+ } catch (error) {
2474
+ console.error("[dashboard-api] Failed to broadcast templates:", error);
2204
2475
  }
2205
- broadcast({
2206
- type: "templates_updated",
2207
- payload: { local, remote }
2208
- });
2209
2476
  };
2210
2477
  router.get("/captures", (req, res) => {
2211
2478
  const limitRaw = typeof req.query.limit === "string" ? req.query.limit : "";
@@ -2418,11 +2685,142 @@ function createDashboardApiRouter(options = {}) {
2418
2685
  return jsonError(res, 400, error?.message || "Run failed");
2419
2686
  }
2420
2687
  });
2688
+ router.post(
2689
+ "/templates/from-capture",
2690
+ import_express.default.json({ limit: "5mb" }),
2691
+ async (req, res) => {
2692
+ const parsed = SaveAsTemplateBodySchema.safeParse(req.body);
2693
+ if (!parsed.success) {
2694
+ return jsonError(
2695
+ res,
2696
+ 400,
2697
+ parsed.error.issues[0]?.message || "Invalid body"
2698
+ );
2699
+ }
2700
+ const { captureId, id, name, event, description, url, overwrite } = parsed.data;
2701
+ if (url !== void 0) {
2702
+ try {
2703
+ new URL(url);
2704
+ } catch {
2705
+ return jsonError(res, 400, "Invalid url");
2706
+ }
2707
+ }
2708
+ const captureFile = replayEngine.getCapture(captureId);
2709
+ if (!captureFile) {
2710
+ return jsonError(res, 404, "Capture not found");
2711
+ }
2712
+ const template = replayEngine.captureToTemplate(captureId, {
2713
+ url,
2714
+ event
2715
+ });
2716
+ try {
2717
+ const result = templateManager.saveUserTemplate(template, {
2718
+ id,
2719
+ name,
2720
+ event: event || template.event,
2721
+ description,
2722
+ overwrite
2723
+ });
2724
+ void broadcastTemplates();
2725
+ return res.json({
2726
+ success: true,
2727
+ id: result.id,
2728
+ filePath: result.filePath,
2729
+ template: result.template
2730
+ });
2731
+ } catch (error) {
2732
+ return jsonError(res, 400, error?.message || "Failed to save template");
2733
+ }
2734
+ }
2735
+ );
2421
2736
  return router;
2422
2737
  }
2423
2738
 
2424
2739
  // src/core/dashboard-server.ts
2425
2740
  var import_meta = {};
2741
+ function isStandaloneBinary() {
2742
+ if (typeof STANDALONE_BINARY !== "undefined" && STANDALONE_BINARY) {
2743
+ return true;
2744
+ }
2745
+ if (typeof globalThis.embeddedDashboardFiles !== "undefined" && globalThis.embeddedDashboardFiles) {
2746
+ return true;
2747
+ }
2748
+ return false;
2749
+ }
2750
+ function getMimeType(filePath) {
2751
+ const ext = import_path4.default.extname(filePath).toLowerCase();
2752
+ const mimeTypes = {
2753
+ ".html": "text/html; charset=utf-8",
2754
+ ".js": "application/javascript; charset=utf-8",
2755
+ ".css": "text/css; charset=utf-8",
2756
+ ".json": "application/json; charset=utf-8",
2757
+ ".png": "image/png",
2758
+ ".jpg": "image/jpeg",
2759
+ ".jpeg": "image/jpeg",
2760
+ ".gif": "image/gif",
2761
+ ".svg": "image/svg+xml",
2762
+ ".ico": "image/x-icon",
2763
+ ".woff": "font/woff",
2764
+ ".woff2": "font/woff2",
2765
+ ".ttf": "font/ttf",
2766
+ ".eot": "application/vnd.ms-fontobject"
2767
+ };
2768
+ return mimeTypes[ext] || "application/octet-stream";
2769
+ }
2770
+ function createEmbeddedDashboardMiddleware() {
2771
+ const filePathMap = /* @__PURE__ */ new Map();
2772
+ let indexHtmlPath = null;
2773
+ if (typeof globalThis.embeddedDashboardFiles !== "undefined") {
2774
+ for (const [key, filePath] of Object.entries(
2775
+ globalThis.embeddedDashboardFiles
2776
+ )) {
2777
+ const servePath = "/" + key.replace(/^dashboard\//, "");
2778
+ filePathMap.set(servePath, filePath);
2779
+ if (servePath === "/index.html") {
2780
+ indexHtmlPath = filePath;
2781
+ }
2782
+ }
2783
+ }
2784
+ const staticMiddleware = async (req, res, next) => {
2785
+ if (!Bun) {
2786
+ return next();
2787
+ }
2788
+ const requestPath = req.path === "/" ? "/index.html" : req.path;
2789
+ const filePath = filePathMap.get(requestPath);
2790
+ if (filePath) {
2791
+ try {
2792
+ const file = Bun.file(filePath);
2793
+ const content = await file.arrayBuffer();
2794
+ res.setHeader("Content-Type", getMimeType(requestPath));
2795
+ res.setHeader("Content-Length", content.byteLength);
2796
+ res.send(Buffer.from(content));
2797
+ } catch (err) {
2798
+ console.error(`Failed to serve embedded file ${requestPath}:`, err);
2799
+ next();
2800
+ }
2801
+ } else {
2802
+ next();
2803
+ }
2804
+ };
2805
+ const spaFallback = async (req, res, next) => {
2806
+ if (req.path.startsWith("/api") || req.path === "/health") {
2807
+ return next();
2808
+ }
2809
+ if (!Bun || !indexHtmlPath) {
2810
+ return next();
2811
+ }
2812
+ try {
2813
+ const file = Bun.file(indexHtmlPath);
2814
+ const content = await file.arrayBuffer();
2815
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2816
+ res.setHeader("Content-Length", content.byteLength);
2817
+ res.send(Buffer.from(content));
2818
+ } catch {
2819
+ next();
2820
+ }
2821
+ };
2822
+ return { staticMiddleware, spaFallback };
2823
+ }
2426
2824
  function resolveDashboardDistDir(runtimeDir) {
2427
2825
  const candidates = [
2428
2826
  // Bundled CLI: dist/index.js -> dist/dashboard
@@ -2476,18 +2874,24 @@ async function startDashboardServer(options = {}) {
2476
2874
  );
2477
2875
  const host = options.host || "localhost";
2478
2876
  const port = options.port ?? 4e3;
2479
- const runtimeDir = typeof __dirname !== "undefined" ? (
2480
- // eslint-disable-next-line no-undef
2481
- __dirname
2482
- ) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url));
2483
- const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2484
- app.use(import_express2.default.static(dashboardDistDir));
2485
- app.get("*", (req, res, next) => {
2486
- if (req.path.startsWith("/api") || req.path === "/health") return next();
2487
- res.sendFile(dashboardIndexHtml, (err) => {
2488
- if (err) next();
2877
+ if (isStandaloneBinary()) {
2878
+ const { staticMiddleware, spaFallback } = createEmbeddedDashboardMiddleware();
2879
+ app.use(staticMiddleware);
2880
+ app.get("*", spaFallback);
2881
+ } else {
2882
+ const runtimeDir = typeof __dirname !== "undefined" ? (
2883
+ // eslint-disable-next-line no-undef
2884
+ __dirname
2885
+ ) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url));
2886
+ const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2887
+ app.use(import_express2.default.static(dashboardDistDir));
2888
+ app.get("*", (req, res, next) => {
2889
+ if (req.path.startsWith("/api") || req.path === "/health") return next();
2890
+ res.sendFile(dashboardIndexHtml, (err) => {
2891
+ if (err) next();
2892
+ });
2489
2893
  });
2490
- });
2894
+ }
2491
2895
  const server = (0, import_http2.createServer)(app);
2492
2896
  const wss = new import_ws2.WebSocketServer({ server, path: "/ws" });
2493
2897
  wss.on("connection", async (ws) => {
@@ -2614,14 +3018,24 @@ var dashboard = new import_commander6.Command().name("dashboard").description("S
2614
3018
 
2615
3019
  // src/index.ts
2616
3020
  var import_meta2 = {};
2617
- var packageJsonPath = (0, import_node_url.fileURLToPath)(
2618
- new URL("../package.json", import_meta2.url)
2619
- );
2620
- var packageJson = JSON.parse(
2621
- (0, import_node_fs.readFileSync)(packageJsonPath, { encoding: "utf8" })
2622
- );
3021
+ function getVersion() {
3022
+ if (typeof CLI_VERSION !== "undefined") {
3023
+ return CLI_VERSION;
3024
+ }
3025
+ try {
3026
+ const packageJsonPath = (0, import_node_url.fileURLToPath)(
3027
+ new URL("../package.json", import_meta2.url)
3028
+ );
3029
+ const packageJson = JSON.parse(
3030
+ (0, import_node_fs.readFileSync)(packageJsonPath, { encoding: "utf8" })
3031
+ );
3032
+ return packageJson.version;
3033
+ } catch {
3034
+ return "0.0.0-unknown";
3035
+ }
3036
+ }
2623
3037
  var program = new import_commander7.Command().name("better-webhook").description(
2624
3038
  "Modern CLI for developing, capturing, and replaying webhooks locally"
2625
- ).version(packageJson.version);
3039
+ ).version(getVersion());
2626
3040
  program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
2627
3041
  program.parseAsync(process.argv);