@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
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
@@ -109,6 +109,12 @@ var ConfigSchema = z.object({
109
109
  // src/core/template-manager.ts
110
110
  var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
111
111
  var TEMPLATES_INDEX_URL = `${GITHUB_RAW_BASE}/templates/templates.json`;
112
+ var TEMPLATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
113
+ function isValidTemplateId(id) {
114
+ if (!id || id.length > 128) return false;
115
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) return false;
116
+ return TEMPLATE_ID_PATTERN.test(id);
117
+ }
112
118
  var TemplateManager = class {
113
119
  baseDir;
114
120
  templatesDir;
@@ -362,6 +368,76 @@ var TemplateManager = class {
362
368
  }
363
369
  return deleted;
364
370
  }
371
+ /**
372
+ * Check if a template with the given ID already exists locally
373
+ */
374
+ templateExists(templateId) {
375
+ return this.getLocalTemplate(templateId) !== null;
376
+ }
377
+ /**
378
+ * Generate a unique template ID from provider and event
379
+ */
380
+ generateTemplateId(provider, event) {
381
+ const providerPart = provider || "custom";
382
+ const eventPart = event || "webhook";
383
+ const baseId = `${providerPart}-${eventPart}`.toLowerCase().replace(/\s+/g, "-");
384
+ if (!this.templateExists(baseId)) {
385
+ return baseId;
386
+ }
387
+ let counter = 1;
388
+ while (this.templateExists(`${baseId}-${counter}`)) {
389
+ counter++;
390
+ }
391
+ return `${baseId}-${counter}`;
392
+ }
393
+ /**
394
+ * Save a user-created template from a captured webhook
395
+ */
396
+ saveUserTemplate(template, options = {}) {
397
+ const provider = template.provider || "custom";
398
+ const event = options.event || template.event || "webhook";
399
+ const templateId = options.id || this.generateTemplateId(provider, event);
400
+ const name = options.name || templateId;
401
+ const description = options.description || template.description;
402
+ if (!isValidTemplateId(templateId)) {
403
+ throw new Error(
404
+ `Invalid template ID "${templateId}". IDs must start with alphanumeric, contain only letters, numbers, dots, underscores, and hyphens.`
405
+ );
406
+ }
407
+ if (!options.overwrite && this.templateExists(templateId)) {
408
+ throw new Error(
409
+ `Template with ID "${templateId}" already exists. Use --overwrite to replace it.`
410
+ );
411
+ }
412
+ const providerDir = join(this.templatesDir, provider);
413
+ if (!existsSync(providerDir)) {
414
+ mkdirSync(providerDir, { recursive: true });
415
+ }
416
+ const metadata = {
417
+ id: templateId,
418
+ name,
419
+ provider,
420
+ event,
421
+ file: `${provider}/${templateId}.json`,
422
+ description,
423
+ source: "capture",
424
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
425
+ };
426
+ const saveData = {
427
+ ...template,
428
+ provider,
429
+ event,
430
+ description,
431
+ _metadata: metadata
432
+ };
433
+ const filePath = join(providerDir, `${templateId}.json`);
434
+ writeFileSync(filePath, JSON.stringify(saveData, null, 2));
435
+ return {
436
+ id: templateId,
437
+ filePath,
438
+ template: saveData
439
+ };
440
+ }
365
441
  };
366
442
  var instance = null;
367
443
  function getTemplateManager(baseDir) {
@@ -1538,6 +1614,7 @@ var capture = new Command3().name("capture").description("Start a server to capt
1538
1614
  import { Command as Command4 } from "commander";
1539
1615
  import chalk4 from "chalk";
1540
1616
  import prompts3 from "prompts";
1617
+ import { homedir as homedir4 } from "os";
1541
1618
 
1542
1619
  // src/core/replay-engine.ts
1543
1620
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
@@ -1671,15 +1748,93 @@ var ReplayEngine = class {
1671
1748
  body = capture2.rawBody;
1672
1749
  }
1673
1750
  }
1751
+ const event = options?.event || this.detectEvent(capture2);
1674
1752
  return {
1675
1753
  url: options?.url || `http://localhost:3000${capture2.path}`,
1676
1754
  method: capture2.method,
1677
1755
  headers,
1678
1756
  body,
1679
1757
  provider: capture2.provider,
1758
+ event,
1680
1759
  description: `Captured ${capture2.provider || "webhook"} at ${capture2.timestamp}`
1681
1760
  };
1682
1761
  }
1762
+ /**
1763
+ * Detect event type from captured webhook headers/body
1764
+ */
1765
+ detectEvent(capture2) {
1766
+ const headers = capture2.headers;
1767
+ const githubEvent = headers["x-github-event"];
1768
+ if (githubEvent) {
1769
+ return Array.isArray(githubEvent) ? githubEvent[0] : githubEvent;
1770
+ }
1771
+ if (capture2.provider === "stripe" && capture2.body) {
1772
+ const body = capture2.body;
1773
+ if (typeof body.type === "string") {
1774
+ return body.type;
1775
+ }
1776
+ }
1777
+ if (capture2.provider === "slack" && capture2.body) {
1778
+ const body = capture2.body;
1779
+ if (typeof body.type === "string") {
1780
+ return body.type;
1781
+ }
1782
+ const event = body.event;
1783
+ if (event && typeof event.type === "string") {
1784
+ return event.type;
1785
+ }
1786
+ }
1787
+ if (capture2.provider === "linear" && capture2.body) {
1788
+ const body = capture2.body;
1789
+ if (typeof body.type === "string") {
1790
+ return body.type;
1791
+ }
1792
+ }
1793
+ if (capture2.provider === "clerk" && capture2.body) {
1794
+ const body = capture2.body;
1795
+ if (typeof body.type === "string") {
1796
+ return body.type;
1797
+ }
1798
+ }
1799
+ if (capture2.provider === "ragie" && capture2.body) {
1800
+ const body = capture2.body;
1801
+ if (typeof body.event_type === "string") {
1802
+ return body.event_type;
1803
+ }
1804
+ }
1805
+ const shopifyTopic = headers["x-shopify-topic"];
1806
+ if (shopifyTopic) {
1807
+ return Array.isArray(shopifyTopic) ? shopifyTopic[0] : shopifyTopic;
1808
+ }
1809
+ if (capture2.provider === "sendgrid" && Array.isArray(capture2.body)) {
1810
+ const firstEvent = capture2.body[0];
1811
+ if (firstEvent && typeof firstEvent.event === "string") {
1812
+ return firstEvent.event;
1813
+ }
1814
+ }
1815
+ if (capture2.provider === "discord" && capture2.body) {
1816
+ const body = capture2.body;
1817
+ if (typeof body.type === "number") {
1818
+ return `type_${body.type}`;
1819
+ }
1820
+ }
1821
+ if (capture2.body && typeof capture2.body === "object") {
1822
+ const body = capture2.body;
1823
+ if (typeof body.type === "string") {
1824
+ return body.type;
1825
+ }
1826
+ if (typeof body.event_type === "string") {
1827
+ return body.event_type;
1828
+ }
1829
+ if (typeof body.event === "string") {
1830
+ return body.event;
1831
+ }
1832
+ if (typeof body.action === "string") {
1833
+ return body.action;
1834
+ }
1835
+ }
1836
+ return void 0;
1837
+ }
1683
1838
  /**
1684
1839
  * Get a summary of a capture
1685
1840
  */
@@ -1768,6 +1923,13 @@ function getReplayEngine(capturesDir) {
1768
1923
  }
1769
1924
 
1770
1925
  // src/commands/captures.ts
1926
+ function toRelativePath(absolutePath) {
1927
+ const home = homedir4();
1928
+ if (absolutePath.startsWith(home)) {
1929
+ return "~" + absolutePath.slice(home.length);
1930
+ }
1931
+ return absolutePath;
1932
+ }
1771
1933
  var listCommand2 = new Command4().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) => {
1772
1934
  const limit = parseInt(options.limit, 10);
1773
1935
  if (isNaN(limit) || limit <= 0) {
@@ -1976,7 +2138,92 @@ var cleanCommand2 = new Command4().name("clean").alias("remove-all").description
1976
2138
  console.log(chalk4.gray(` Storage: ${engine.getCapturesDir()}
1977
2139
  `));
1978
2140
  });
1979
- var captures = new Command4().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2);
2141
+ var saveAsTemplateCommand = new Command4().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(
2142
+ async (captureId, options) => {
2143
+ const engine = getReplayEngine();
2144
+ const templateManager = getTemplateManager();
2145
+ const captureFile = engine.getCapture(captureId);
2146
+ if (!captureFile) {
2147
+ console.log(chalk4.red(`
2148
+ \u274C Capture not found: ${captureId}
2149
+ `));
2150
+ process.exitCode = 1;
2151
+ return;
2152
+ }
2153
+ const { capture: capture2 } = captureFile;
2154
+ console.log(chalk4.bold("\n\u{1F4CB} Capture to save as template:\n"));
2155
+ console.log(` ${chalk4.white(capture2.id.slice(0, 8))}`);
2156
+ console.log(chalk4.gray(` ${capture2.method} ${capture2.path}`));
2157
+ if (capture2.provider) {
2158
+ console.log(chalk4.gray(` Provider: ${capture2.provider}`));
2159
+ }
2160
+ console.log();
2161
+ const template = engine.captureToTemplate(captureId, {
2162
+ url: options.url,
2163
+ event: options.event
2164
+ });
2165
+ let templateId = options.id;
2166
+ if (!templateId) {
2167
+ const suggestedId = `${capture2.provider || "custom"}-${template.event || "webhook"}`.toLowerCase().replace(/\s+/g, "-");
2168
+ const response = await prompts3({
2169
+ type: "text",
2170
+ name: "templateId",
2171
+ message: "Template ID:",
2172
+ initial: suggestedId,
2173
+ validate: (value) => value.trim().length > 0 || "Template ID is required"
2174
+ });
2175
+ if (!response.templateId) {
2176
+ console.log(chalk4.yellow("Cancelled"));
2177
+ return;
2178
+ }
2179
+ templateId = response.templateId;
2180
+ }
2181
+ if (!options.overwrite && templateId && templateManager.templateExists(templateId)) {
2182
+ const response = await prompts3({
2183
+ type: "confirm",
2184
+ name: "overwrite",
2185
+ message: `Template "${templateId}" already exists. Overwrite?`,
2186
+ initial: false
2187
+ });
2188
+ if (!response.overwrite) {
2189
+ console.log(chalk4.yellow("Cancelled"));
2190
+ return;
2191
+ }
2192
+ options.overwrite = true;
2193
+ }
2194
+ try {
2195
+ const result = templateManager.saveUserTemplate(template, {
2196
+ id: templateId,
2197
+ name: options.name,
2198
+ event: options.event || template.event,
2199
+ description: options.description,
2200
+ overwrite: options.overwrite
2201
+ });
2202
+ console.log(chalk4.green(`
2203
+ \u2713 Saved template: ${result.id}`));
2204
+ console.log(chalk4.gray(` File: ${toRelativePath(result.filePath)}`));
2205
+ console.log(chalk4.gray(` Provider: ${template.provider || "custom"}`));
2206
+ if (template.event) {
2207
+ console.log(chalk4.gray(` Event: ${template.event}`));
2208
+ }
2209
+ console.log();
2210
+ console.log(chalk4.gray(" Run it with:"));
2211
+ console.log(
2212
+ chalk4.cyan(
2213
+ ` better-webhook run ${result.id} --url http://localhost:3000/webhooks
2214
+ `
2215
+ )
2216
+ );
2217
+ } catch (error) {
2218
+ const message = error instanceof Error ? error.message : "Failed to save template";
2219
+ console.log(chalk4.red(`
2220
+ \u274C ${message}
2221
+ `));
2222
+ process.exitCode = 1;
2223
+ }
2224
+ }
2225
+ );
2226
+ var captures = new Command4().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2).addCommand(saveAsTemplateCommand);
1980
2227
 
1981
2228
  // src/commands/replay.ts
1982
2229
  import { Command as Command5 } from "commander";
@@ -2163,12 +2410,28 @@ var ReplayBodySchema = z2.object({
2163
2410
  var TemplateDownloadBodySchema = z2.object({
2164
2411
  id: z2.string().min(1)
2165
2412
  });
2413
+ var TemplateIdSchema = z2.string().regex(
2414
+ /^[a-z0-9][a-z0-9._-]*$/i,
2415
+ "ID must start with alphanumeric and contain only letters, numbers, dots, underscores, and hyphens"
2416
+ ).max(128, "ID must be 128 characters or less").refine(
2417
+ (val) => !val.includes("/") && !val.includes("\\") && !val.includes(".."),
2418
+ "ID cannot contain path separators or parent directory references"
2419
+ );
2166
2420
  var RunTemplateBodySchema = z2.object({
2167
2421
  templateId: z2.string().min(1),
2168
2422
  url: z2.string().min(1),
2169
2423
  secret: z2.string().optional(),
2170
2424
  headers: z2.array(HeaderEntrySchema).optional()
2171
2425
  });
2426
+ var SaveAsTemplateBodySchema = z2.object({
2427
+ captureId: z2.string().min(1),
2428
+ id: TemplateIdSchema.optional(),
2429
+ name: z2.string().optional(),
2430
+ event: z2.string().optional(),
2431
+ description: z2.string().optional(),
2432
+ url: z2.string().optional(),
2433
+ overwrite: z2.boolean().optional()
2434
+ });
2172
2435
  function createDashboardApiRouter(options = {}) {
2173
2436
  const router = express.Router();
2174
2437
  const replayEngine = new ReplayEngine(options.capturesDir);
@@ -2184,22 +2447,26 @@ function createDashboardApiRouter(options = {}) {
2184
2447
  };
2185
2448
  const broadcastTemplates = async () => {
2186
2449
  if (!broadcast) return;
2187
- const local = templateManager.listLocalTemplates();
2188
- let remote = [];
2189
2450
  try {
2190
- const index = await templateManager.fetchRemoteIndex(false);
2191
- const localIds = new Set(local.map((t) => t.id));
2192
- remote = index.templates.map((metadata) => ({
2193
- metadata,
2194
- isDownloaded: localIds.has(metadata.id)
2195
- }));
2196
- } catch {
2197
- remote = [];
2451
+ const local = templateManager.listLocalTemplates();
2452
+ let remote = [];
2453
+ try {
2454
+ const index = await templateManager.fetchRemoteIndex(false);
2455
+ const localIds = new Set(local.map((t) => t.id));
2456
+ remote = index.templates.map((metadata) => ({
2457
+ metadata,
2458
+ isDownloaded: localIds.has(metadata.id)
2459
+ }));
2460
+ } catch {
2461
+ remote = [];
2462
+ }
2463
+ broadcast({
2464
+ type: "templates_updated",
2465
+ payload: { local, remote }
2466
+ });
2467
+ } catch (error) {
2468
+ console.error("[dashboard-api] Failed to broadcast templates:", error);
2198
2469
  }
2199
- broadcast({
2200
- type: "templates_updated",
2201
- payload: { local, remote }
2202
- });
2203
2470
  };
2204
2471
  router.get("/captures", (req, res) => {
2205
2472
  const limitRaw = typeof req.query.limit === "string" ? req.query.limit : "";
@@ -2412,10 +2679,141 @@ function createDashboardApiRouter(options = {}) {
2412
2679
  return jsonError(res, 400, error?.message || "Run failed");
2413
2680
  }
2414
2681
  });
2682
+ router.post(
2683
+ "/templates/from-capture",
2684
+ express.json({ limit: "5mb" }),
2685
+ async (req, res) => {
2686
+ const parsed = SaveAsTemplateBodySchema.safeParse(req.body);
2687
+ if (!parsed.success) {
2688
+ return jsonError(
2689
+ res,
2690
+ 400,
2691
+ parsed.error.issues[0]?.message || "Invalid body"
2692
+ );
2693
+ }
2694
+ const { captureId, id, name, event, description, url, overwrite } = parsed.data;
2695
+ if (url !== void 0) {
2696
+ try {
2697
+ new URL(url);
2698
+ } catch {
2699
+ return jsonError(res, 400, "Invalid url");
2700
+ }
2701
+ }
2702
+ const captureFile = replayEngine.getCapture(captureId);
2703
+ if (!captureFile) {
2704
+ return jsonError(res, 404, "Capture not found");
2705
+ }
2706
+ const template = replayEngine.captureToTemplate(captureId, {
2707
+ url,
2708
+ event
2709
+ });
2710
+ try {
2711
+ const result = templateManager.saveUserTemplate(template, {
2712
+ id,
2713
+ name,
2714
+ event: event || template.event,
2715
+ description,
2716
+ overwrite
2717
+ });
2718
+ void broadcastTemplates();
2719
+ return res.json({
2720
+ success: true,
2721
+ id: result.id,
2722
+ filePath: result.filePath,
2723
+ template: result.template
2724
+ });
2725
+ } catch (error) {
2726
+ return jsonError(res, 400, error?.message || "Failed to save template");
2727
+ }
2728
+ }
2729
+ );
2415
2730
  return router;
2416
2731
  }
2417
2732
 
2418
2733
  // src/core/dashboard-server.ts
2734
+ function isStandaloneBinary() {
2735
+ if (typeof STANDALONE_BINARY !== "undefined" && STANDALONE_BINARY) {
2736
+ return true;
2737
+ }
2738
+ if (typeof globalThis.embeddedDashboardFiles !== "undefined" && globalThis.embeddedDashboardFiles) {
2739
+ return true;
2740
+ }
2741
+ return false;
2742
+ }
2743
+ function getMimeType(filePath) {
2744
+ const ext = path.extname(filePath).toLowerCase();
2745
+ const mimeTypes = {
2746
+ ".html": "text/html; charset=utf-8",
2747
+ ".js": "application/javascript; charset=utf-8",
2748
+ ".css": "text/css; charset=utf-8",
2749
+ ".json": "application/json; charset=utf-8",
2750
+ ".png": "image/png",
2751
+ ".jpg": "image/jpeg",
2752
+ ".jpeg": "image/jpeg",
2753
+ ".gif": "image/gif",
2754
+ ".svg": "image/svg+xml",
2755
+ ".ico": "image/x-icon",
2756
+ ".woff": "font/woff",
2757
+ ".woff2": "font/woff2",
2758
+ ".ttf": "font/ttf",
2759
+ ".eot": "application/vnd.ms-fontobject"
2760
+ };
2761
+ return mimeTypes[ext] || "application/octet-stream";
2762
+ }
2763
+ function createEmbeddedDashboardMiddleware() {
2764
+ const filePathMap = /* @__PURE__ */ new Map();
2765
+ let indexHtmlPath = null;
2766
+ if (typeof globalThis.embeddedDashboardFiles !== "undefined") {
2767
+ for (const [key, filePath] of Object.entries(
2768
+ globalThis.embeddedDashboardFiles
2769
+ )) {
2770
+ const servePath = "/" + key.replace(/^dashboard\//, "");
2771
+ filePathMap.set(servePath, filePath);
2772
+ if (servePath === "/index.html") {
2773
+ indexHtmlPath = filePath;
2774
+ }
2775
+ }
2776
+ }
2777
+ const staticMiddleware = async (req, res, next) => {
2778
+ if (!Bun) {
2779
+ return next();
2780
+ }
2781
+ const requestPath = req.path === "/" ? "/index.html" : req.path;
2782
+ const filePath = filePathMap.get(requestPath);
2783
+ if (filePath) {
2784
+ try {
2785
+ const file = Bun.file(filePath);
2786
+ const content = await file.arrayBuffer();
2787
+ res.setHeader("Content-Type", getMimeType(requestPath));
2788
+ res.setHeader("Content-Length", content.byteLength);
2789
+ res.send(Buffer.from(content));
2790
+ } catch (err) {
2791
+ console.error(`Failed to serve embedded file ${requestPath}:`, err);
2792
+ next();
2793
+ }
2794
+ } else {
2795
+ next();
2796
+ }
2797
+ };
2798
+ const spaFallback = async (req, res, next) => {
2799
+ if (req.path.startsWith("/api") || req.path === "/health") {
2800
+ return next();
2801
+ }
2802
+ if (!Bun || !indexHtmlPath) {
2803
+ return next();
2804
+ }
2805
+ try {
2806
+ const file = Bun.file(indexHtmlPath);
2807
+ const content = await file.arrayBuffer();
2808
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2809
+ res.setHeader("Content-Length", content.byteLength);
2810
+ res.send(Buffer.from(content));
2811
+ } catch {
2812
+ next();
2813
+ }
2814
+ };
2815
+ return { staticMiddleware, spaFallback };
2816
+ }
2419
2817
  function resolveDashboardDistDir(runtimeDir) {
2420
2818
  const candidates = [
2421
2819
  // Bundled CLI: dist/index.js -> dist/dashboard
@@ -2469,18 +2867,24 @@ async function startDashboardServer(options = {}) {
2469
2867
  );
2470
2868
  const host = options.host || "localhost";
2471
2869
  const port = options.port ?? 4e3;
2472
- const runtimeDir = typeof __dirname !== "undefined" ? (
2473
- // eslint-disable-next-line no-undef
2474
- __dirname
2475
- ) : path.dirname(fileURLToPath(import.meta.url));
2476
- const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2477
- app.use(express2.static(dashboardDistDir));
2478
- app.get("*", (req, res, next) => {
2479
- if (req.path.startsWith("/api") || req.path === "/health") return next();
2480
- res.sendFile(dashboardIndexHtml, (err) => {
2481
- if (err) next();
2870
+ if (isStandaloneBinary()) {
2871
+ const { staticMiddleware, spaFallback } = createEmbeddedDashboardMiddleware();
2872
+ app.use(staticMiddleware);
2873
+ app.get("*", spaFallback);
2874
+ } else {
2875
+ const runtimeDir = typeof __dirname !== "undefined" ? (
2876
+ // eslint-disable-next-line no-undef
2877
+ __dirname
2878
+ ) : path.dirname(fileURLToPath(import.meta.url));
2879
+ const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2880
+ app.use(express2.static(dashboardDistDir));
2881
+ app.get("*", (req, res, next) => {
2882
+ if (req.path.startsWith("/api") || req.path === "/health") return next();
2883
+ res.sendFile(dashboardIndexHtml, (err) => {
2884
+ if (err) next();
2885
+ });
2482
2886
  });
2483
- });
2887
+ }
2484
2888
  const server = createServer2(app);
2485
2889
  const wss = new WebSocketServer2({ server, path: "/ws" });
2486
2890
  wss.on("connection", async (ws) => {
@@ -2606,14 +3010,24 @@ var dashboard = new Command6().name("dashboard").description("Start the local da
2606
3010
  });
2607
3011
 
2608
3012
  // src/index.ts
2609
- var packageJsonPath = fileURLToPath2(
2610
- new URL("../package.json", import.meta.url)
2611
- );
2612
- var packageJson = JSON.parse(
2613
- readFileSync4(packageJsonPath, { encoding: "utf8" })
2614
- );
3013
+ function getVersion() {
3014
+ if (typeof CLI_VERSION !== "undefined") {
3015
+ return CLI_VERSION;
3016
+ }
3017
+ try {
3018
+ const packageJsonPath = fileURLToPath2(
3019
+ new URL("../package.json", import.meta.url)
3020
+ );
3021
+ const packageJson = JSON.parse(
3022
+ readFileSync5(packageJsonPath, { encoding: "utf8" })
3023
+ );
3024
+ return packageJson.version;
3025
+ } catch {
3026
+ return "0.0.0-unknown";
3027
+ }
3028
+ }
2615
3029
  var program = new Command7().name("better-webhook").description(
2616
3030
  "Modern CLI for developing, capturing, and replaying webhooks locally"
2617
- ).version(packageJson.version);
3031
+ ).version(getVersion());
2618
3032
  program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
2619
3033
  program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-webhook/cli",
3
- "version": "3.4.4",
3
+ "version": "3.6.0",
4
4
  "description": "Modern CLI for developing, capturing, and replaying webhooks locally with dashboard UI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,11 +55,12 @@
55
55
  "express": "^4.21.0",
56
56
  "ora": "^8.1.0",
57
57
  "prompts": "^2.4.2",
58
- "undici": "^7.16.0",
58
+ "undici": "^7.18.2",
59
59
  "ws": "^8.18.0",
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,8 +73,11 @@
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
- "lint": "tsc"
80
+ "lint": "tsc",
81
+ "test": "vitest run"
78
82
  }
79
83
  }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare const capture: Command;
@@ -1,30 +0,0 @@
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
- });
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare const captures: Command;