hookwright 1.0.0 → 1.1.1

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 (3) hide show
  1. package/README.md +34 -8
  2. package/dist/cli.js +1154 -345
  3. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ var init_package = __esm({
24
24
  "package.json"() {
25
25
  package_default = {
26
26
  name: "hookwright",
27
- version: "1.0.0",
27
+ version: "1.1.1",
28
28
  description: "Build real, signed e-commerce webhooks from a live Shopify catalogue \u2014 interactive terminal UI, no backend",
29
29
  keywords: [
30
30
  "webhook",
@@ -63,6 +63,7 @@ var init_package = __esm({
63
63
  react: "^19.2.8"
64
64
  },
65
65
  devDependencies: {
66
+ "curl-to-postmanv2": "^1.8.7",
66
67
  esbuild: "^0.28.2"
67
68
  }
68
69
  };
@@ -70,11 +71,11 @@ var init_package = __esm({
70
71
  });
71
72
 
72
73
  // src/cli.jsx
73
- import React15 from "react";
74
+ import React16 from "react";
74
75
  import { render } from "ink";
75
76
 
76
77
  // src/ui/App.jsx
77
- import React14, { useState as useState8 } from "react";
78
+ import React15, { useState as useState8 } from "react";
78
79
  import { useApp as useApp2 } from "ink";
79
80
 
80
81
  // src/ui/screens/Home.jsx
@@ -330,8 +331,8 @@ function Home({ config, onPick, onQuit }) {
330
331
  const ready = canSend(config);
331
332
  const shop = config.shopify.shop;
332
333
  const options = [
333
- { label: "Setup".padEnd(18) + (ready ? "Shopify and webhook destination" : "connect Shopify and set the webhook URL"), value: "setup" },
334
334
  { label: "Integrations".padEnd(18) + "build and send a provider webhook", value: "integrations" },
335
+ { label: "Setup".padEnd(18) + (ready ? "Shopify and webhook destination" : "connect Shopify and set the webhook URL"), value: "setup" },
335
336
  { label: "History".padEnd(18) + "review or re-send a saved payload", value: "history" },
336
337
  { label: "Doctor".padEnd(18) + "check credentials and connectivity", value: "doctor" },
337
338
  { label: "Clear".padEnd(18) + "remove cached data", value: "clear" },
@@ -551,6 +552,20 @@ var editableFields = [
551
552
  { group: "UTM", path: "data.utm_parameters.utm_content", label: "UTM content", optional: true },
552
553
  { group: "UTM", path: "data.utm_parameters.fbclid", label: "Facebook click id", optional: true }
553
554
  ];
555
+ function summarize(payload) {
556
+ const d = payload?.data ?? {};
557
+ const items = Array.isArray(d.line_items) ? d.line_items : [];
558
+ return {
559
+ event: "abandoned checkout",
560
+ store: d.store_url,
561
+ customer: `${d.customer?.shipping_address?.customer_name ?? ""} \xB7 ${d.phone ?? ""}`.trim(),
562
+ items: items.map((i) => `${i.name} x${i.quantity}`).join(", "),
563
+ total: d.total_price,
564
+ currency: items[0]?.currency,
565
+ extra: `was ${d.original_total_price}, discount ${d.total_discount}`,
566
+ link: d.abandoned_checkout_url
567
+ };
568
+ }
554
569
  var cashfree_occ_default = {
555
570
  id: "cashfree-occ",
556
571
  urlKey: "cashfree_occ",
@@ -564,7 +579,461 @@ var cashfree_occ_default = {
564
579
  buildPayload,
565
580
  sign,
566
581
  webhookUrl,
567
- webhookSecret
582
+ webhookSecret,
583
+ summarize
584
+ };
585
+
586
+ // src/providers/razorpay-magic.mjs
587
+ import crypto2 from "node:crypto";
588
+ var GATE_PATH2 = "abandoned_checkout_url";
589
+ var fieldMap2 = [
590
+ { source: "abandoned_checkout_url", target: "\xABgate\xBB + abandonedCheckoutUrl", required: true, note: "must be non-empty or the event is dropped" },
591
+ { source: "created_at", target: "eventTime" },
592
+ { source: "shop_id", target: "storeUrl", required: true, note: "shopify_domain / store_url also accepted" },
593
+ { source: "email", target: "email" },
594
+ { source: "phone", target: "phone", required: true, note: "falls back to Shipping_address.Phone" },
595
+ { source: "currency", target: "pricing.currency", required: true, note: "read from the ROOT, unlike Cashfree" },
596
+ { source: "line_items_total", target: "pricing.totalPrice", required: true, note: "documented as a STRING in paise \u2014 the consumer parseFloats it and divides by 100" },
597
+ { source: "platform", target: "\u2014", note: "documented root field" },
598
+ { source: "utm_parameters", target: "\u2014", note: "documented root field" },
599
+ { source: "customer.first_name", target: "customer.firstName" },
600
+ { source: "customer.last_name", target: "customer.lastName" },
601
+ { source: "customer.email", target: "customer.email" },
602
+ { source: "customer.Shipping_address.Name", target: "customer.name" },
603
+ { source: "customer.Shipping_address.First_name", target: "customer.firstName fallback" },
604
+ { source: "customer.Shipping_address.Last_name", target: "customer.lastName fallback" },
605
+ { source: "customer.Shipping_address.Phone", target: "phone fallback" },
606
+ { source: "customer.Shipping_address.Address1", target: "customer.address1" },
607
+ { source: "customer.Shipping_address.Address2", target: "customer.address2" },
608
+ { source: "customer.Shipping_address.City", target: "customer.city" },
609
+ { source: "customer.Shipping_address.Province", target: "customer.province" },
610
+ { source: "customer.Shipping_address.Country_name", target: "customer.country" },
611
+ { source: "customer.Shipping_address.Country_code", target: "customer.countryCode", required: true, note: "region used to parse the phone" },
612
+ { source: "customer.Shipping_address.Zip", target: "customer.zip" },
613
+ { source: "line_items[0].name", target: "cart.productName", required: true },
614
+ { source: "line_items[0].image_url", target: "cart.image", required: true },
615
+ { source: "line_items[0].quantity", target: "cart.quantity" }
616
+ ];
617
+ var editableFields2 = [
618
+ { group: "Checkout", path: "abandoned_checkout_url", label: "Abandoned checkout URL", hint: "must be non-empty \u2014 this is the schema gate" },
619
+ { group: "Checkout", path: "created_at", label: "Created at" },
620
+ { group: "Checkout", path: "shop_id", label: "Shop domain" },
621
+ { group: "Checkout", path: "cart_token", label: "Cart token" },
622
+ { group: "Pricing", path: "line_items_total", label: "Line items total (paise, string)", hint: "documented as a string; the consumer divides by 100" },
623
+ { group: "Pricing", path: "currency", label: "Currency" },
624
+ { group: "Customer", path: "customer.first_name", label: "First name" },
625
+ { group: "Customer", path: "customer.last_name", label: "Last name" },
626
+ { group: "Customer", path: "email", label: "Email" },
627
+ { group: "Customer", path: "phone", label: "Phone" },
628
+ { group: "Shipping", path: "customer.Shipping_address.Name", label: "Ship-to name" },
629
+ { group: "Shipping", path: "customer.Shipping_address.Address1", label: "Address line 1" },
630
+ { group: "Shipping", path: "customer.Shipping_address.Address2", label: "Address line 2", optional: true },
631
+ { group: "Shipping", path: "customer.Shipping_address.City", label: "City" },
632
+ { group: "Shipping", path: "customer.Shipping_address.Province", label: "Province / state" },
633
+ { group: "Shipping", path: "customer.Shipping_address.Province_code", label: "Province code", optional: true },
634
+ { group: "Shipping", path: "customer.Shipping_address.Country_name", label: "Country" },
635
+ { group: "Shipping", path: "customer.Shipping_address.Country_code", label: "Country code", hint: "region used to parse the phone" },
636
+ { group: "Shipping", path: "customer.Shipping_address.Zip", label: "Zip / postcode" }
637
+ ];
638
+ function buildPayload2(ctx) {
639
+ const { config, items, phone } = ctx;
640
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
641
+ const currency = ctx.currency || config.defaults.currency;
642
+ const domain = config.shopify.domain;
643
+ const cust = config.customer;
644
+ const lineItems = items.map(({ product, variant, quantity }) => {
645
+ const price = money2(variant?.price ?? 0);
646
+ return {
647
+ image_url: imageForVariant(product, variant),
648
+ name: product.title,
649
+ title: product.title,
650
+ price,
651
+ variant_price: price,
652
+ product_id: String(product.id),
653
+ variant_id: variant ? String(variant.id) : null,
654
+ variant_title: variant?.title ?? null,
655
+ quantity,
656
+ sku: variant?.sku || null,
657
+ tax_amount: 0,
658
+ taxable: true,
659
+ store_name: config.shopify.shop?.name ?? domain,
660
+ gram: "0"
661
+ };
662
+ });
663
+ const total = money2(lineItems.reduce((sum, li) => sum + li.price * li.quantity, 0));
664
+ const discount = money2(Math.min(ctx.discount ?? 0, total));
665
+ const net2 = money2(total - discount);
666
+ const checkoutUrl = cartPermalink(domain, items.map(({ variant, quantity }) => ({ variantId: variant?.id, quantity })));
667
+ return {
668
+ shop_id: domain,
669
+ shopify_domain: domain,
670
+ platform: config.defaults.platform,
671
+ token: crypto2.randomBytes(16).toString("hex"),
672
+ cart_token: crypto2.randomBytes(16).toString("hex"),
673
+ email: cust.email,
674
+ phone,
675
+ abandoned_checkout_url: checkoutUrl,
676
+ currency,
677
+ // Documented as a string, in the smallest currency unit (paise).
678
+ // The consumer parses it with parseFloat and divides by 100.
679
+ line_items_total: String(Math.round(net2 * 100)),
680
+ line_items: lineItems,
681
+ tax_details: {},
682
+ promotions: [],
683
+ utm_parameters: { ...config.defaults.utm },
684
+ // Not in the documented field list, but the consumer reads it for eventTime.
685
+ created_at: now.toISOString(),
686
+ customer: {
687
+ email: cust.email,
688
+ first_name: cust.firstName,
689
+ last_name: cust.lastName,
690
+ created_at: now.toISOString(),
691
+ Shipping_address: {
692
+ Name: `${cust.firstName} ${cust.lastName}`.trim(),
693
+ First_name: cust.firstName,
694
+ Last_name: cust.lastName,
695
+ Phone: phone,
696
+ Address1: cust.address1,
697
+ Address2: cust.address2,
698
+ City: cust.city,
699
+ Province: cust.province,
700
+ Province_code: cust.provinceCode,
701
+ Country: cust.country,
702
+ Country_name: cust.country,
703
+ Country_code: cust.countryCode,
704
+ Zip: cust.zip
705
+ }
706
+ }
707
+ };
708
+ }
709
+ function sign2({ body, secret }) {
710
+ const signature = crypto2.createHmac("sha256", String(secret)).update(body).digest("hex");
711
+ return {
712
+ signature,
713
+ timestamp: null,
714
+ headers: {
715
+ "content-type": "application/json",
716
+ "x-razorpay-signature": signature
717
+ }
718
+ };
719
+ }
720
+ function webhookUrl2(config) {
721
+ return String(targetFor(config, "razorpay-magic").webhookUrl ?? "").trim();
722
+ }
723
+ function webhookSecret2(config) {
724
+ return String(targetFor(config, "razorpay-magic").webhookSecret ?? "");
725
+ }
726
+ function summarize2(payload) {
727
+ const items = Array.isArray(payload?.line_items) ? payload.line_items : [];
728
+ const ship = payload?.customer?.Shipping_address ?? {};
729
+ return {
730
+ event: "abandoned checkout",
731
+ store: payload?.shop_id,
732
+ customer: `${ship.Name ?? ""} \xB7 ${payload?.phone ?? ""}`.trim(),
733
+ items: items.map((i) => `${i.name} x${i.quantity}`).join(", "),
734
+ total: Number.parseFloat(payload?.line_items_total ?? "0") / 100,
735
+ currency: payload?.currency,
736
+ extra: `${payload?.line_items_total} paise`,
737
+ link: payload?.abandoned_checkout_url
738
+ };
739
+ }
740
+ var razorpay_magic_default = {
741
+ id: "razorpay-magic",
742
+ urlKey: "razorpay_magic",
743
+ label: "Razorpay Magic",
744
+ eventType: "razorpay_magic_abandoned_checkout",
745
+ gate: { path: GATE_PATH2, value: void 0, nonEmpty: true },
746
+ signatureScheme: "hex HMAC-SHA256 over the body",
747
+ signatureHeader: "x-razorpay-signature",
748
+ fieldMap: fieldMap2,
749
+ editableFields: editableFields2,
750
+ buildPayload: buildPayload2,
751
+ sign: sign2,
752
+ webhookUrl: webhookUrl2,
753
+ webhookSecret: webhookSecret2,
754
+ summarize: summarize2
755
+ };
756
+
757
+ // src/providers/nitro.mjs
758
+ import crypto3 from "node:crypto";
759
+ var EVENTS = [
760
+ { eventName: "view", type: "nitro_view", label: "Page View" },
761
+ { eventName: "category_view", type: "nitro_category_view", label: "Category View" },
762
+ { eventName: "product_view", type: "nitro_product_view", label: "Product View" },
763
+ { eventName: "addtocart", type: "nitro_addtocart", label: "Add To Cart" },
764
+ { eventName: "removefromcart", type: "nitro_removefromcart", label: "Remove From Cart" },
765
+ { eventName: "checkout", type: "nitro_checkout", label: "Checkout" },
766
+ { eventName: "orders/create", type: "nitro_orders_create", label: "Order Created" },
767
+ { eventName: "orders/updated", type: "nitro_orders_updated", label: "Order Updated" }
768
+ ];
769
+ var DEFAULT_EVENT = "addtocart";
770
+ var COMMON_FIELDS = [
771
+ { source: "eventName", target: "\xABgate\xBB", required: true, note: "must match the schema exactly" },
772
+ { source: "country", target: "phone region", required: true, note: "used to parse eventVal.customer.phone" },
773
+ { source: "eventVal.customer.phone", target: "phone", required: true, note: "missing phone is rejected on every event" }
774
+ ];
775
+ var PAGE_FIELDS = [
776
+ { source: "eventVal.page", target: "eventVal.page" },
777
+ { source: "eventVal.h", target: "eventVal.h", note: "host" },
778
+ { source: "eventVal.l", target: "eventVal.l", note: "location" },
779
+ { source: "eventVal._ss", target: "eventVal._ss", note: "session" }
780
+ ];
781
+ var FIELD_MAPS = {
782
+ view: [...COMMON_FIELDS, ...PAGE_FIELDS],
783
+ category_view: [
784
+ ...COMMON_FIELDS,
785
+ ...PAGE_FIELDS,
786
+ { source: "eventVal.resource_id", target: "eventVal.resource_id", required: true },
787
+ { source: "eventVal.resource", target: "eventVal.resource" },
788
+ { source: "eventVal.domain", target: "eventVal.domain" }
789
+ ],
790
+ product_view: [
791
+ ...COMMON_FIELDS,
792
+ ...PAGE_FIELDS,
793
+ { source: "eventVal.title", target: "eventVal.title", required: true },
794
+ { source: "eventVal.image", target: "eventVal.image", required: true },
795
+ { source: "eventVal.price", target: "eventVal.price", required: true },
796
+ { source: "eventVal.resource_id", target: "eventVal.resource_id", required: true },
797
+ { source: "eventVal.resource", target: "eventVal.resource" },
798
+ { source: "eventVal.domain", target: "eventVal.domain" }
799
+ ],
800
+ addtocart: [
801
+ ...COMMON_FIELDS,
802
+ { source: "eventVal.cart_value", target: "eventVal.cart_value", required: true },
803
+ { source: "eventVal.init_cart", target: "eventVal.init_cart" },
804
+ { source: "eventVal.empty_cart", target: "eventVal.empty_cart" },
805
+ { source: "eventVal.bik_customer_id", target: "eventVal.bik_customer_id" },
806
+ { source: "eventVal.line_items[0].name", target: "cart enrichment", required: true }
807
+ ],
808
+ removefromcart: [
809
+ ...COMMON_FIELDS,
810
+ { source: "eventVal.cart_value", target: "eventVal.cart_value", required: true },
811
+ { source: "eventVal.init_cart", target: "eventVal.init_cart" },
812
+ { source: "eventVal.empty_cart", target: "eventVal.empty_cart" },
813
+ { source: "eventVal.bik_customer_id", target: "eventVal.bik_customer_id" },
814
+ { source: "eventVal.line_items[0].name", target: "cart enrichment" }
815
+ ],
816
+ checkout: [
817
+ ...COMMON_FIELDS,
818
+ { source: "eventVal.checkout", target: "eventVal.checkout", required: true },
819
+ { source: "eventVal.cart_value", target: "eventVal.cart_value", required: true },
820
+ { source: "eventVal.bik_customer_id", target: "eventVal.bik_customer_id" }
821
+ ],
822
+ "orders/create": [
823
+ ...COMMON_FIELDS,
824
+ { source: "eventVal.order_id", target: "eventVal.order_id", required: true },
825
+ { source: "eventVal.order_number", target: "eventVal.order_number" },
826
+ { source: "eventVal.order_name", target: "eventVal.order_name" },
827
+ { source: "eventVal.price", target: "eventVal.price", required: true },
828
+ { source: "eventVal.currency", target: "eventVal.currency", required: true },
829
+ { source: "eventVal.url", target: "eventVal.url" },
830
+ { source: "eventVal.order_created_at", target: "eventVal.order_created_at" },
831
+ { source: "eventVal.bik_customer_id", target: "eventVal.bik_customer_id" }
832
+ ]
833
+ };
834
+ FIELD_MAPS["orders/updated"] = FIELD_MAPS["orders/create"];
835
+ var fieldMap3 = FIELD_MAPS[DEFAULT_EVENT];
836
+ var EDITABLE = {
837
+ common: [
838
+ { group: "Event", path: "eventName", label: "Event name", hint: "the schema gate \u2014 must match exactly" },
839
+ { group: "Event", path: "country", label: "Country code", hint: "region used to parse the phone" },
840
+ { group: "Event", path: "timestamp", label: "Timestamp" },
841
+ { group: "Customer", path: "eventVal.customer.phone", label: "Phone", hint: "required on every Nitro event" },
842
+ { group: "Customer", path: "eventVal.customer.email", label: "Email" },
843
+ { group: "Customer", path: "eventVal.customer.name", label: "Name" }
844
+ ],
845
+ page: [
846
+ { group: "Page", path: "eventVal.page", label: "Page" },
847
+ { group: "Page", path: "eventVal.h", label: "Host" },
848
+ { group: "Page", path: "eventVal.l", label: "Location URL" },
849
+ { group: "Page", path: "eventVal._ss", label: "Session id" }
850
+ ],
851
+ cart: [
852
+ { group: "Cart", path: "eventVal.cart_value", label: "Cart value", type: "number" },
853
+ { group: "Cart", path: "eventVal.init_cart", label: "Init cart" },
854
+ { group: "Cart", path: "eventVal.empty_cart", label: "Empty cart" },
855
+ { group: "Cart", path: "eventVal.bik_customer_id", label: "Bik customer id" }
856
+ ],
857
+ order: [
858
+ { group: "Order", path: "eventVal.order_id", label: "Order id" },
859
+ { group: "Order", path: "eventVal.order_number", label: "Order number" },
860
+ { group: "Order", path: "eventVal.order_name", label: "Order name" },
861
+ { group: "Order", path: "eventVal.price", label: "Price", type: "number" },
862
+ { group: "Order", path: "eventVal.currency", label: "Currency" },
863
+ { group: "Order", path: "eventVal.url", label: "Order URL" }
864
+ ]
865
+ };
866
+ function editableFieldsFor(eventName) {
867
+ const e = EDITABLE;
868
+ if (eventName === "view") return [...e.common, ...e.page];
869
+ if (eventName === "category_view" || eventName === "product_view") return [...e.common, ...e.page];
870
+ if (eventName === "addtocart" || eventName === "removefromcart") return [...e.common, ...e.cart];
871
+ if (eventName === "checkout") return [...e.common, ...e.cart];
872
+ if (eventName.startsWith("orders/")) return [...e.common, ...e.order];
873
+ return e.common;
874
+ }
875
+ var editableFields3 = editableFieldsFor(DEFAULT_EVENT);
876
+ function buildPayload3(ctx) {
877
+ const { config, items, phone } = ctx;
878
+ const eventName = ctx.eventName ?? DEFAULT_EVENT;
879
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
880
+ const currency = ctx.currency || config.defaults.currency;
881
+ const domain = config.shopify.domain;
882
+ const cust = config.customer;
883
+ const session = crypto3.randomBytes(8).toString("hex");
884
+ const first = items[0];
885
+ const lineItems = items.map(({ product, variant, quantity }) => ({
886
+ id: variant ? String(variant.id) : null,
887
+ product_id: String(product.id),
888
+ name: product.title,
889
+ title: product.title,
890
+ quantity,
891
+ price: money2(variant?.price ?? 0),
892
+ image: imageForVariant(product, variant),
893
+ url: `https://${domain}/products/${product.handle ?? ""}`
894
+ }));
895
+ const cartValue = money2(lineItems.reduce((sum, li) => sum + li.price * li.quantity, 0));
896
+ const productUrl = first ? `https://${domain}/products/${first.product.handle ?? ""}` : `https://${domain}`;
897
+ const customer = {
898
+ phone,
899
+ email: cust.email,
900
+ name: `${cust.firstName} ${cust.lastName}`.trim(),
901
+ first_name: cust.firstName,
902
+ last_name: cust.lastName
903
+ };
904
+ const base = {
905
+ org_token: config.defaults.orgToken ?? crypto3.randomBytes(12).toString("hex"),
906
+ eventName,
907
+ userId: crypto3.randomUUID(),
908
+ timestamp: now.toISOString(),
909
+ country: cust.countryCode,
910
+ domain
911
+ };
912
+ const page = {
913
+ page: eventName === "product_view" ? "product" : eventName === "category_view" ? "collection" : "home",
914
+ h: domain,
915
+ l: productUrl,
916
+ _ss: session,
917
+ domain
918
+ };
919
+ let eventVal;
920
+ switch (eventName) {
921
+ case "view":
922
+ eventVal = { ...page, page: "home", l: `https://${domain}`, customer };
923
+ break;
924
+ case "category_view":
925
+ eventVal = {
926
+ ...page,
927
+ resource: "collection",
928
+ resource_id: String(first?.product?.id ?? ""),
929
+ l: `https://${domain}/collections/all`,
930
+ customer
931
+ };
932
+ break;
933
+ case "product_view":
934
+ eventVal = {
935
+ ...page,
936
+ resource: "product",
937
+ resource_id: String(first?.product?.id ?? ""),
938
+ title: first?.product?.title,
939
+ image: first ? imageForVariant(first.product, first.variant) : void 0,
940
+ price: money2(first?.variant?.price ?? 0),
941
+ customer
942
+ };
943
+ break;
944
+ case "addtocart":
945
+ case "removefromcart":
946
+ eventVal = {
947
+ cart_value: cartValue,
948
+ init_cart: eventName === "addtocart",
949
+ empty_cart: eventName === "removefromcart" && items.length === 0,
950
+ bik_customer_id: crypto3.randomUUID(),
951
+ line_items: lineItems,
952
+ [eventName === "addtocart" ? "items_added" : "items_removed"]: lineItems,
953
+ customer
954
+ };
955
+ break;
956
+ case "checkout":
957
+ eventVal = {
958
+ checkout: cartPermalink(domain, items.map(({ variant, quantity }) => ({ variantId: variant?.id, quantity }))),
959
+ cart_value: cartValue,
960
+ bik_customer_id: crypto3.randomUUID(),
961
+ line_items: lineItems,
962
+ customer
963
+ };
964
+ break;
965
+ case "orders/create":
966
+ case "orders/updated": {
967
+ const orderNumber = 1e3 + Math.floor(Math.random() * 9e3);
968
+ eventVal = {
969
+ url: `https://${domain}/admin/orders/${orderNumber}`,
970
+ order_id: String(Date.now()),
971
+ order_number: orderNumber,
972
+ order_name: `#${orderNumber}`,
973
+ price: cartValue,
974
+ currency,
975
+ order_created_at: now.toISOString(),
976
+ bik_customer_id: crypto3.randomUUID(),
977
+ line_items: lineItems,
978
+ customer
979
+ };
980
+ break;
981
+ }
982
+ default:
983
+ eventVal = { customer };
984
+ }
985
+ return { ...base, eventVal };
986
+ }
987
+ function sign3({ secret }) {
988
+ return {
989
+ signature: null,
990
+ timestamp: null,
991
+ headers: {
992
+ "content-type": "application/json",
993
+ authorization: `Bearer ${secret}`
994
+ }
995
+ };
996
+ }
997
+ function webhookUrl3(config) {
998
+ return String(targetFor(config, "nitro").webhookUrl ?? "").trim();
999
+ }
1000
+ function webhookSecret3(config) {
1001
+ return String(targetFor(config, "nitro").webhookSecret ?? "");
1002
+ }
1003
+ function summarize3(payload) {
1004
+ const ev = payload?.eventVal ?? {};
1005
+ const items = Array.isArray(ev.line_items) ? ev.line_items : [];
1006
+ return {
1007
+ event: payload?.eventName,
1008
+ store: payload?.domain,
1009
+ customer: `${ev.customer?.name ?? ""} \xB7 ${ev.customer?.phone ?? ""}`.trim(),
1010
+ items: items.map((i) => `${i.name} x${i.quantity}`).join(", ") || "\u2014",
1011
+ total: ev.cart_value ?? ev.price ?? void 0,
1012
+ currency: ev.currency,
1013
+ extra: ev.order_name ? `order ${ev.order_name}` : ev.checkout ? "checkout started" : "",
1014
+ link: ev.checkout ?? ev.url ?? ev.l
1015
+ };
1016
+ }
1017
+ var nitro_default = {
1018
+ id: "nitro",
1019
+ urlKey: "nitro",
1020
+ label: "Nitro",
1021
+ events: EVENTS,
1022
+ defaultEvent: DEFAULT_EVENT,
1023
+ eventType: "nitro_addtocart",
1024
+ gate: { path: "eventName", value: DEFAULT_EVENT },
1025
+ gateFor: (eventName) => ({ path: "eventName", value: eventName }),
1026
+ signatureScheme: "static bearer token (no body signature)",
1027
+ signatureHeader: "authorization",
1028
+ fieldMap: fieldMap3,
1029
+ fieldMapFor: (eventName) => FIELD_MAPS[eventName] ?? FIELD_MAPS[DEFAULT_EVENT],
1030
+ editableFields: editableFields3,
1031
+ editableFieldsFor,
1032
+ buildPayload: buildPayload3,
1033
+ sign: sign3,
1034
+ webhookUrl: webhookUrl3,
1035
+ webhookSecret: webhookSecret3,
1036
+ summarize: summarize3
568
1037
  };
569
1038
 
570
1039
  // src/providers/index.mjs
@@ -581,18 +1050,21 @@ var PROVIDER_META = [
581
1050
  label: "Razorpay Magic",
582
1051
  signature: "hex HMAC-SHA256 over the body",
583
1052
  header: "x-razorpay-signature",
584
- available: false
1053
+ available: true
585
1054
  },
586
1055
  {
587
1056
  id: "nitro",
588
1057
  label: "Nitro",
589
1058
  signature: "static bearer token",
590
1059
  header: "authorization",
591
- available: false
1060
+ available: true,
1061
+ events: nitro_default.events
592
1062
  }
593
1063
  ];
594
1064
  var providers = {
595
- [cashfree_occ_default.id]: cashfree_occ_default
1065
+ [cashfree_occ_default.id]: cashfree_occ_default,
1066
+ [razorpay_magic_default.id]: razorpay_magic_default,
1067
+ [nitro_default.id]: nitro_default
596
1068
  };
597
1069
  function getProvider(id) {
598
1070
  const provider = providers[id];
@@ -671,8 +1143,8 @@ function Integrations({ config, onPick, onBack, onSetup }) {
671
1143
  }
672
1144
 
673
1145
  // src/ui/screens/Build.jsx
674
- import React7, { useEffect, useState } from "react";
675
- import { Box as Box7, Text as Text7, useApp, useInput as useInput3 } from "ink";
1146
+ import React8, { useEffect, useState } from "react";
1147
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput3 } from "ink";
676
1148
  import { Spinner, Select as Select4, MultiSelect, TextInput, ConfirmInput, Alert, Badge } from "@inkjs/ui";
677
1149
 
678
1150
  // src/ui/components/CheckList.jsx
@@ -723,6 +1195,40 @@ function FieldTable({ coverage: coverage2, limit }) {
723
1195
  ] });
724
1196
  }
725
1197
 
1198
+ // src/ui/components/CommandBar.jsx
1199
+ import React7 from "react";
1200
+ import { Box as Box7, Text as Text7 } from "ink";
1201
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1202
+ function CommandBar({ commands, width: width2 = 78 }) {
1203
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
1204
+ /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "\u2500".repeat(width2) }),
1205
+ /* @__PURE__ */ jsx7(Box7, { flexWrap: "wrap", children: commands.map((c2, i) => /* @__PURE__ */ jsxs7(Box7, { marginRight: 2, children: [
1206
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: c2.disabled ? palette.dim : c2.active ? palette.ok : palette.accent, children: c2.key }),
1207
+ /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: ` ${c2.label}` }),
1208
+ i < commands.length - 1 ? /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: " \xB7" }) : null
1209
+ ] }, c2.key)) })
1210
+ ] });
1211
+ }
1212
+ function StatusRow({ label, ok, warn, value, detail, labelWidth = 12 }) {
1213
+ const colour = ok === void 0 ? palette.dim : ok ? palette.ok : warn ? palette.warn : palette.bad;
1214
+ const mark = ok === void 0 ? " " : ok ? "\u2714" : warn ? "\u25B2" : "\u2716";
1215
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
1216
+ /* @__PURE__ */ jsx7(Text7, { color: colour, children: ` ${mark} ` }),
1217
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: String(label).padEnd(labelWidth) }),
1218
+ /* @__PURE__ */ jsx7(Text7, { color: colour, children: value }),
1219
+ detail ? /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: ` ${detail}` }) : null
1220
+ ] });
1221
+ }
1222
+ function Panel({ title, hint, children }) {
1223
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
1224
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1225
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: palette.heading, children: title }),
1226
+ hint ? /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: ` ${hint}` }) : null
1227
+ ] }),
1228
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: palette.dim, paddingX: 1, children })
1229
+ ] });
1230
+ }
1231
+
726
1232
  // src/errors.mjs
727
1233
  var ForgeError = class extends Error {
728
1234
  constructor(message, { hint, cause } = {}) {
@@ -988,6 +1494,113 @@ function setPath(obj, path6, value) {
988
1494
  return obj;
989
1495
  }
990
1496
 
1497
+ // src/core/reachability.mjs
1498
+ import dns from "node:dns/promises";
1499
+ import net from "node:net";
1500
+ function parseEndpoint(url) {
1501
+ const raw = String(url ?? "").trim();
1502
+ if (!raw) return { ok: false, reason: "no webhook URL configured" };
1503
+ let parsed;
1504
+ try {
1505
+ parsed = new URL(raw);
1506
+ } catch {
1507
+ return { ok: false, reason: `"${raw}" is not a valid URL` };
1508
+ }
1509
+ if (!/^https?:$/.test(parsed.protocol)) {
1510
+ return { ok: false, reason: `unsupported protocol "${parsed.protocol}" \u2014 use http or https` };
1511
+ }
1512
+ if (!parsed.hostname) return { ok: false, reason: "URL has no host" };
1513
+ return { ok: true, url: parsed };
1514
+ }
1515
+ async function resolves(hostname) {
1516
+ if (net.isIP(hostname)) return { ok: true, address: hostname };
1517
+ try {
1518
+ const { address } = await dns.lookup(hostname);
1519
+ return { ok: true, address };
1520
+ } catch (err) {
1521
+ if (err.code === "ENOTFOUND" || err.code === "EAI_AGAIN") {
1522
+ return { ok: false, code: err.code, reason: `host "${hostname}" does not resolve` };
1523
+ }
1524
+ return { ok: false, code: err.code, reason: `DNS lookup failed: ${err.message}` };
1525
+ }
1526
+ }
1527
+ function connects(host, port, timeoutMs) {
1528
+ return new Promise((resolve) => {
1529
+ const socket = net.connect({ host, port });
1530
+ const done = (result) => {
1531
+ socket.removeAllListeners();
1532
+ socket.destroy();
1533
+ resolve(result);
1534
+ };
1535
+ socket.setTimeout(timeoutMs);
1536
+ socket.once("connect", () => done({ ok: true }));
1537
+ socket.once("timeout", () => done({ ok: false, timedOut: true }));
1538
+ socket.once("error", (err) => done({ ok: false, code: err.code, message: err.message }));
1539
+ });
1540
+ }
1541
+ async function respond(url, method, timeoutMs) {
1542
+ const controller = new AbortController();
1543
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1544
+ try {
1545
+ const res = await fetch(url, { method, signal: controller.signal, redirect: "manual" });
1546
+ clearTimeout(timer);
1547
+ return { responded: true, status: res.status };
1548
+ } catch (err) {
1549
+ clearTimeout(timer);
1550
+ return {
1551
+ responded: false,
1552
+ aborted: err.name === "AbortError",
1553
+ code: err.cause?.code ?? err.code,
1554
+ message: err.message
1555
+ };
1556
+ }
1557
+ }
1558
+ async function checkEndpointExists(url, { timeoutMs = 8e3 } = {}) {
1559
+ const name = "Endpoint exists";
1560
+ const parsed = parseEndpoint(url);
1561
+ if (!parsed.ok) return { name, ok: false, exists: false, detail: parsed.reason };
1562
+ const target = parsed.url;
1563
+ const dnsResult = await resolves(target.hostname);
1564
+ if (!dnsResult.ok) {
1565
+ return { name, ok: false, exists: false, detail: `${dnsResult.reason} \u2014 check the URL for a typo` };
1566
+ }
1567
+ const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);
1568
+ const tcp = await connects(dnsResult.address, port, timeoutMs);
1569
+ if (!tcp.ok && tcp.code === "ECONNREFUSED") {
1570
+ return { name, ok: false, exists: false, detail: `${target.host} refused the connection on port ${port} \u2014 nothing is listening there` };
1571
+ }
1572
+ if (!tcp.ok && (tcp.code === "EHOSTUNREACH" || tcp.code === "ENETUNREACH")) {
1573
+ return { name, ok: false, exists: false, detail: `${target.host} is unreachable (${tcp.code})` };
1574
+ }
1575
+ if (!tcp.ok && tcp.timedOut) {
1576
+ return { name, ok: true, warn: true, exists: void 0, detail: `${target.host}:${port} did not accept a connection within ${timeoutMs}ms \u2014 sending anyway` };
1577
+ }
1578
+ let attempt = await respond(target, "OPTIONS", timeoutMs);
1579
+ if (!attempt.responded) attempt = await respond(target, "HEAD", timeoutMs);
1580
+ if (attempt.responded) {
1581
+ if (attempt.status === 404) {
1582
+ return {
1583
+ name,
1584
+ ok: false,
1585
+ exists: true,
1586
+ detail: `${target.host} is reachable but returned 404 for ${target.pathname} \u2014 the host is right, the path is not`
1587
+ };
1588
+ }
1589
+ return { name, ok: true, exists: true, detail: `${target.host} responded (HTTP ${attempt.status}) at ${target.pathname}` };
1590
+ }
1591
+ if (attempt.aborted) {
1592
+ return { name, ok: true, warn: true, exists: void 0, detail: `${target.host} did not answer within ${timeoutMs}ms \u2014 sending anyway` };
1593
+ }
1594
+ const code = attempt.code;
1595
+ if (code === "ENOTFOUND") {
1596
+ return { name, ok: false, exists: false, detail: `${target.host} does not resolve` };
1597
+ }
1598
+ if (code === "CERT_HAS_EXPIRED" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || code === "DEPTH_ZERO_SELF_SIGNED_CERT") {
1599
+ return { name, ok: true, warn: true, exists: true, detail: `${target.host} is up but its TLS certificate is not trusted (${code})` };
1600
+ }
1601
+ return { name, ok: true, warn: true, exists: void 0, detail: `could not confirm ${target.host} (${code ?? attempt.message}) \u2014 sending anyway` };
1602
+ }
1603
+
991
1604
  // src/core/validate.mjs
992
1605
  function checkRoundTrip(body) {
993
1606
  let reparsed;
@@ -1006,20 +1619,45 @@ function checkRoundTrip(body) {
1006
1619
  detail: `re-serialized body differs at offset ${at.index}: sent ${JSON.stringify(at.a)} vs recomputed ${JSON.stringify(at.b)}`
1007
1620
  };
1008
1621
  }
1009
- function checkGate(payload, provider) {
1010
- const actual = getPath(payload, provider.gate.path);
1011
- const ok = actual === provider.gate.value;
1622
+ function checkGate(payload, provider, eventName) {
1623
+ const gate = eventName && provider.gateFor ? provider.gateFor(eventName) : provider.gate;
1624
+ const actual = getPath(payload, gate.path);
1625
+ if (gate.nonEmpty) {
1626
+ const ok2 = Boolean(actual);
1627
+ return {
1628
+ name: "Schema gate",
1629
+ ok: ok2,
1630
+ detail: ok2 ? `${gate.path} is set` : `${gate.path} is empty \u2014 the schema filter requires a non-empty value, so the event would be silently ignored`
1631
+ };
1632
+ }
1633
+ const ok = actual === gate.value;
1012
1634
  return {
1013
1635
  name: "Schema gate",
1014
1636
  ok,
1015
- detail: ok ? `${provider.gate.path} = "${actual}"` : `${provider.gate.path} is ${JSON.stringify(actual)} but the schema filter requires "${provider.gate.value}" \u2014 the event would be silently ignored`
1637
+ detail: ok ? `${gate.path} = "${actual}"` : `${gate.path} is ${JSON.stringify(actual)} but the schema filter requires "${gate.value}" \u2014 the event would be silently ignored`
1016
1638
  };
1017
1639
  }
1640
+ function locatePhone(payload) {
1641
+ const candidates = [
1642
+ // cashfree-occ
1643
+ ["data.phone", "data.customer.shipping_address.country_code"],
1644
+ ["data.customer.shipping_address.phone", "data.customer.shipping_address.country_code"],
1645
+ // razorpay-magic
1646
+ ["phone", "customer.Shipping_address.Country_code"],
1647
+ ["customer.Shipping_address.Phone", "customer.Shipping_address.Country_code"],
1648
+ // nitro
1649
+ ["eventVal.customer.phone", "country"]
1650
+ ];
1651
+ for (const [phonePath, regionPath] of candidates) {
1652
+ const phone = getPath(payload, phonePath);
1653
+ if (phone) return { phone, region: getPath(payload, regionPath) };
1654
+ }
1655
+ return { phone: void 0, region: void 0 };
1656
+ }
1018
1657
  function checkPhone(payload) {
1019
- const phone = getPath(payload, "data.phone") ?? getPath(payload, "data.customer.shipping_address.phone");
1020
- const region = getPath(payload, "data.customer.shipping_address.country_code");
1658
+ const { phone, region } = locatePhone(payload);
1021
1659
  if (!phone) {
1022
- return { name: "Phone", ok: false, detail: "no phone in data.phone or shipping_address.phone \u2014 consumer throws BadPayloadError" };
1660
+ return { name: "Phone", ok: false, detail: "no phone found in the payload \u2014 the consumer throws BadPayloadError" };
1023
1661
  }
1024
1662
  const parsed = toE164(phone, region);
1025
1663
  return {
@@ -1033,8 +1671,8 @@ function checkAllowedPhone(payload, config) {
1033
1671
  if (!allowed.length) {
1034
1672
  return { name: "Phone allow-list", ok: true, detail: "no allow-list configured (skipped)", warn: true };
1035
1673
  }
1036
- const phone = getPath(payload, "data.phone");
1037
- const parsed = toE164(phone, getPath(payload, "data.customer.shipping_address.country_code"));
1674
+ const { phone, region } = locatePhone(payload);
1675
+ const parsed = toE164(phone, region);
1038
1676
  const normalized = parsed.ok ? parsed.value : phone;
1039
1677
  const ok = allowed.some((p) => toE164(p, "IN").value === normalized || p === normalized);
1040
1678
  return {
@@ -1043,8 +1681,8 @@ function checkAllowedPhone(payload, config) {
1043
1681
  detail: ok ? `${normalized} is an approved test handset` : `${normalized} is NOT in allowedPhones \u2014 refusing to risk messaging a real shopper`
1044
1682
  };
1045
1683
  }
1046
- function coverage(payload, fieldMap2) {
1047
- return fieldMap2.map((f) => {
1684
+ function coverage(payload, fieldMap4) {
1685
+ return fieldMap4.map((f) => {
1048
1686
  const value = getPath(payload, f.source);
1049
1687
  const present = value !== void 0 && value !== null && value !== "";
1050
1688
  return {
@@ -1058,10 +1696,10 @@ function coverage(payload, fieldMap2) {
1058
1696
  });
1059
1697
  }
1060
1698
  async function checkImages(payload, { timeoutMs = 1e4 } = {}) {
1061
- const items = getPath(payload, "data.line_items") ?? [];
1062
- const urls = [...new Set(items.map((i) => i?.image_url).filter(Boolean))];
1699
+ const items = getPath(payload, "data.line_items") ?? getPath(payload, "line_items") ?? getPath(payload, "eventVal.line_items") ?? [];
1700
+ const urls = [...new Set(items.map((i) => i?.image_url ?? i?.image).filter(Boolean))];
1063
1701
  if (!urls.length) {
1064
- return [{ name: "Product images", ok: false, detail: "no line item carries an image_url \u2014 cart.image resolves to undefined" }];
1702
+ return [{ name: "Product images", ok: true, warn: true, detail: "no line item carries an image \u2014 cart.image will be undefined" }];
1065
1703
  }
1066
1704
  const results = await Promise.all(
1067
1705
  urls.map(async (url) => {
@@ -1083,10 +1721,12 @@ async function checkImages(payload, { timeoutMs = 1e4 } = {}) {
1083
1721
  detail: `${r.ok ? "HTTP " + r.status : "unreachable (" + (r.error ?? r.status) + ")"} \u2014 ${shorten(r.url)}`
1084
1722
  }));
1085
1723
  }
1086
- async function runAll({ payload, body, provider, config, checkImageUrls = true }) {
1087
- const checks = [checkGate(payload, provider), checkRoundTrip(body), checkPhone(payload), checkAllowedPhone(payload, config)];
1724
+ async function runAll({ payload, body, provider, config, checkImageUrls = true, checkEndpoint = true, eventName }) {
1725
+ const checks = [checkGate(payload, provider, eventName), checkRoundTrip(body), checkPhone(payload), checkAllowedPhone(payload, config)];
1088
1726
  if (checkImageUrls) checks.push(...await checkImages(payload));
1089
- const cov = coverage(payload, provider.fieldMap);
1727
+ if (checkEndpoint) checks.push(await checkEndpointExists(provider.webhookUrl(config)));
1728
+ const fields = eventName && provider.fieldMapFor ? provider.fieldMapFor(eventName) : provider.fieldMap;
1729
+ const cov = coverage(payload, fields);
1090
1730
  const missing = cov.filter((c2) => c2.status === "MISSING");
1091
1731
  checks.push({
1092
1732
  name: "Required field coverage",
@@ -1168,18 +1808,21 @@ function loadPayload(file) {
1168
1808
  }
1169
1809
 
1170
1810
  // src/core/build.mjs
1171
- function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone }) {
1811
+ function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, eventName }) {
1172
1812
  const provider = getProvider(providerId);
1813
+ const event = eventName ?? provider.defaultEvent;
1173
1814
  const resolvedPhone = phone ?? cfg.customer.phone;
1174
1815
  const parsed = toE164(resolvedPhone, cfg.customer.countryCode);
1175
1816
  const payload = provider.buildPayload({
1176
1817
  config: cfg,
1177
1818
  items,
1178
1819
  discount,
1820
+ eventName: event,
1179
1821
  phone: parsed.ok ? parsed.value : String(resolvedPhone ?? "")
1180
1822
  });
1181
- const fields = (provider.editableFields ?? []).map((f) => ({ ...f, value: getPath(payload, f.path) }));
1182
- return { provider, payload, fields };
1823
+ const editable = provider.editableFieldsFor ? provider.editableFieldsFor(event) : provider.editableFields;
1824
+ const fields = (editable ?? []).map((f) => ({ ...f, value: getPath(payload, f.path) }));
1825
+ return { provider, payload, fields, eventName: event };
1183
1826
  }
1184
1827
  function applyEdits(payload, fields, edits) {
1185
1828
  for (const field of fields) {
@@ -1190,15 +1833,17 @@ function applyEdits(payload, fields, edits) {
1190
1833
  }
1191
1834
  return payload;
1192
1835
  }
1193
- async function buildPayload2({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, checkImageUrls = true, payload: prebuilt }) {
1836
+ async function buildPayload4({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, checkImageUrls = true, payload: prebuilt, eventName }) {
1194
1837
  if (!items?.length) throw new ConfigError("no products selected");
1195
1838
  const provider = getProvider(providerId);
1196
1839
  const resolvedPhone = phone ?? cfg.customer.phone;
1197
1840
  const parsed = toE164(resolvedPhone, cfg.customer.countryCode);
1198
1841
  if (!parsed.ok) throw new ConfigError(`phone is unusable: ${parsed.reason}`);
1199
- const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value });
1842
+ const event = eventName ?? provider.defaultEvent;
1843
+ const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value, eventName: event });
1200
1844
  const body = JSON.stringify(payload);
1201
- const report = await runAll({ payload, body, provider, config: cfg, checkImageUrls });
1845
+ const report = await runAll({ payload, body, provider, config: cfg, checkImageUrls, eventName: event });
1846
+ const summary = provider.summarize ? provider.summarize(payload) : {};
1202
1847
  const endpoint = provider.webhookUrl(cfg);
1203
1848
  let host = "";
1204
1849
  try {
@@ -1208,17 +1853,18 @@ async function buildPayload2({ cfg, providerId = "cashfree-occ", items, discount
1208
1853
  }
1209
1854
  const meta = {
1210
1855
  provider: provider.id,
1856
+ event,
1211
1857
  endpoint,
1212
1858
  environment: host,
1213
1859
  store: cfg.shopify.domain,
1214
1860
  items: items.map((i) => ({ title: i.product.title, variant: i.variant?.title, qty: i.quantity, price: i.variant?.price })),
1215
- totalPrice: payload.data.total_price,
1216
- currency: payload.data.line_items[0]?.currency,
1861
+ totalPrice: summary.total,
1862
+ currency: summary.currency,
1217
1863
  valid: report.ok
1218
1864
  };
1219
1865
  const file = savePayload({ provider: provider.id, payload, body, meta });
1220
1866
  audit("payload.built", { provider: provider.id, valid: report.ok, file });
1221
- return { provider, payload, body, report, file, meta };
1867
+ return { provider, payload, body, report, file, meta, summary };
1222
1868
  }
1223
1869
 
1224
1870
  // src/core/send.mjs
@@ -1230,18 +1876,32 @@ function looksLikeProduction(origin) {
1230
1876
  }
1231
1877
  function buildRequest({ provider, config, body, timestamp }) {
1232
1878
  const url = provider.webhookUrl(config);
1879
+ if (!url) {
1880
+ const err = new Error(`no webhook URL configured for ${provider.label}`);
1881
+ err.hint = "set it in Setup \u2192 Webhook (or edit targets in config.json)";
1882
+ throw err;
1883
+ }
1233
1884
  const secret = provider.webhookSecret ? provider.webhookSecret(config) : "";
1234
1885
  const signed = provider.sign({ body, secret, timestamp });
1235
1886
  return { url, headers: signed.headers, signature: signed.signature, timestamp: signed.timestamp, body };
1236
1887
  }
1237
- function toCurl({ url, headers, bodyFile }) {
1238
- const lines = [`curl -X POST '${url}' \\`];
1888
+ function shellQuote(value) {
1889
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
1890
+ }
1891
+ function toCurl({ url, headers, body, method = "POST" }) {
1892
+ const lines = [`curl --location --request ${method} ${shellQuote(url)} \\`];
1239
1893
  for (const [k, v] of Object.entries(headers)) {
1240
- lines.push(` -H '${k}: ${v}' \\`);
1894
+ lines.push(`--header ${shellQuote(`${k}: ${v}`)} \\`);
1241
1895
  }
1242
- lines.push(` --data-binary @${bodyFile}`);
1896
+ lines.push(`--data ${shellQuote(body)}`);
1243
1897
  return lines.join("\n");
1244
1898
  }
1899
+ function writeCurlFile(command, name = "last-curl.sh") {
1900
+ fs4.mkdirSync(STATE_DIR, { recursive: true });
1901
+ const file = path4.join(STATE_DIR, name);
1902
+ fs4.writeFileSync(file, command + "\n");
1903
+ return file;
1904
+ }
1245
1905
  function writeBodyFile(body, name = "last-body.json") {
1246
1906
  fs4.mkdirSync(STATE_DIR, { recursive: true });
1247
1907
  const file = path4.join(STATE_DIR, name);
@@ -1279,28 +1939,42 @@ async function send({ url, headers, body, timeoutMs = 2e4 }) {
1279
1939
  }
1280
1940
 
1281
1941
  // src/core/dispatch.mjs
1282
- async function dispatch({ cfg, record, force = false, dryRun = false, revalidate = true }) {
1942
+ async function dispatch({ cfg, record, force = false, dryRun = false, revalidate = true, checkEndpoint = true }) {
1283
1943
  const rec = record ?? loadLast();
1284
1944
  if (!rec) throw new ForgeError("nothing to send", { hint: "build a payload first" });
1285
1945
  const provider = getProvider(rec.provider);
1286
1946
  let report = null;
1287
1947
  if (revalidate) {
1288
- report = await runAll({ payload: rec.payload, body: rec.body, provider, config: cfg, checkImageUrls: false });
1948
+ report = await runAll({ payload: rec.payload, body: rec.body, provider, config: cfg, checkImageUrls: false, eventName: rec.meta?.event });
1289
1949
  if (!report.ok && !force) {
1290
- throw new ForgeError("refusing to send a payload that fails pre-flight", {
1291
- hint: `blocking: ${report.blocking.map((b) => b.name).join(", ")} \u2014 pass --force to override`
1950
+ const err = new ForgeError("refusing to send a payload that fails pre-flight", {
1951
+ hint: "fix the problems below, or pass --force to send regardless"
1292
1952
  });
1953
+ err.failures = report.blocking;
1954
+ throw err;
1293
1955
  }
1294
1956
  }
1295
1957
  const request = buildRequest({ provider, config: cfg, body: rec.body });
1296
1958
  if (looksLikeProduction(request.url) && !force) {
1297
1959
  throw new ForgeError(`target looks like production: ${request.url}`, { hint: "pass --force if you really mean it" });
1298
1960
  }
1961
+ let endpoint = null;
1962
+ if (checkEndpoint && !dryRun) {
1963
+ endpoint = await checkEndpointExists(request.url);
1964
+ if (!endpoint.ok && !force) {
1965
+ const err = new ForgeError(`endpoint check failed: ${endpoint.detail}`, {
1966
+ hint: "fix the URL in Setup \u2192 Webhook, or pass --force to send regardless"
1967
+ });
1968
+ err.failures = [endpoint];
1969
+ throw err;
1970
+ }
1971
+ }
1299
1972
  const bodyFile = writeBodyFile(rec.body);
1300
- const curl = toCurl({ url: request.url, headers: request.headers, bodyFile });
1301
- if (dryRun) return { dryRun: true, request, bodyFile, curl, report, provider };
1973
+ const curl = toCurl({ url: request.url, headers: request.headers, body: rec.body });
1974
+ const curlFile = writeCurlFile(curl);
1975
+ if (dryRun) return { dryRun: true, request, bodyFile, curl, curlFile, report, provider, endpoint };
1302
1976
  const result = await send({ url: request.url, headers: request.headers, body: rec.body });
1303
- return { ...result, dryRun: false, request, bodyFile, curl, report, provider };
1977
+ return { ...result, dryRun: false, request, bodyFile, curl, curlFile, report, provider, endpoint };
1304
1978
  }
1305
1979
  function explainStatus(status) {
1306
1980
  if (status >= 200 && status < 300) return "accepted \u2014 a 2xx only means it was received; check eventData to confirm the event landed";
@@ -1362,8 +2036,9 @@ async function copy(text, { allowOsc52 = true } = {}) {
1362
2036
  }
1363
2037
 
1364
2038
  // src/ui/screens/Build.jsx
1365
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2039
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1366
2040
  var STEP = {
2041
+ EVENT: "event",
1367
2042
  LOADING: "loading",
1368
2043
  PICK: "pick",
1369
2044
  VARIANT: "variant",
@@ -1380,7 +2055,10 @@ var STEP = {
1380
2055
  };
1381
2056
  function Build({ config, providerId = "cashfree-occ", onDone }) {
1382
2057
  const { exit } = useApp();
1383
- const [step, setStep] = useState(STEP.LOADING);
2058
+ const provider = getProvider(providerId);
2059
+ const multiEvent = Array.isArray(provider.events) && provider.events.length > 1;
2060
+ const [eventName, setEventName] = useState(provider.defaultEvent);
2061
+ const [step, setStep] = useState(multiEvent ? STEP.EVENT : STEP.LOADING);
1384
2062
  const [products, setProducts] = useState([]);
1385
2063
  const [chosen, setChosen] = useState([]);
1386
2064
  const [cursor, setCursor] = useState(0);
@@ -1391,18 +2069,69 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1391
2069
  const [edits, setEdits] = useState({});
1392
2070
  const [built, setBuilt] = useState(null);
1393
2071
  const [copied, setCopied] = useState(null);
1394
- const [showPayload, setShowPayload] = useState(true);
2072
+ const [request, setRequest] = useState(null);
2073
+ const [curl, setCurl] = useState("");
2074
+ const [panel, setPanel] = useState(null);
1395
2075
  const [sendResult, setSendResult] = useState(null);
1396
2076
  const [error, setError] = useState(null);
2077
+ const goBack = () => {
2078
+ switch (step) {
2079
+ case STEP.EVENT:
2080
+ return onDone();
2081
+ case STEP.PICK:
2082
+ return multiEvent ? setStep(STEP.EVENT) : onDone();
2083
+ case STEP.VARIANT:
2084
+ if (cursor === 0) return setStep(STEP.PICK);
2085
+ setItems(items.slice(0, -1));
2086
+ return stepInto(cursor - 1);
2087
+ case STEP.QUANTITY:
2088
+ if (chosen[cursor]?.variants.length > 1) return setStep(STEP.VARIANT);
2089
+ if (cursor === 0) return setStep(STEP.PICK);
2090
+ setItems(items.slice(0, -1));
2091
+ return stepInto(cursor - 1);
2092
+ case STEP.DISCOUNT: {
2093
+ const last = chosen.length - 1;
2094
+ setItems(items.slice(0, -1));
2095
+ return stepInto(last);
2096
+ }
2097
+ case STEP.MODE:
2098
+ return setStep(STEP.DISCOUNT);
2099
+ case STEP.FIELDS:
2100
+ if (fieldIndex === 0) return setStep(STEP.MODE);
2101
+ return setFieldIndex(fieldIndex - 1);
2102
+ case STEP.REPORT:
2103
+ return setStep(STEP.MODE);
2104
+ case STEP.RESULT:
2105
+ case STEP.ERROR:
2106
+ return onDone();
2107
+ default:
2108
+ return onDone();
2109
+ }
2110
+ };
2111
+ const stepInto = (index) => {
2112
+ setCursor(index);
2113
+ const product = chosen[index];
2114
+ if (!product) return setStep(STEP.PICK);
2115
+ if (product.variants.length > 1) return setStep(STEP.VARIANT);
2116
+ setPendingVariant(product.variants[0]);
2117
+ setStep(STEP.QUANTITY);
2118
+ };
1397
2119
  useInput3((input, key) => {
1398
- if (key.escape) onDone();
2120
+ if (key.escape) return goBack();
1399
2121
  if ((step === STEP.RESULT || step === STEP.ERROR) && (key.return || input === "q")) onDone();
1400
- if (step === STEP.REPORT && input === "p") setShowPayload((v) => !v);
1401
- if (step === STEP.REPORT && input === "c" && built) {
1402
- copy(built.body).then(setCopied);
1403
- }
2122
+ if (step !== STEP.REPORT) return;
2123
+ const toggle = (name) => setPanel((v) => v === name ? null : name);
2124
+ if (input === "p") toggle("payload");
2125
+ if (input === "f") toggle("fields");
2126
+ if (input === "v") toggle("checks");
2127
+ if (input === "r") toggle("request");
2128
+ if (input === "u") toggle("curl");
2129
+ if (input === "c" && curl) copy(curl).then((r) => setCopied({ ...r, what: "curl" }));
2130
+ if (input === "j" && built) copy(built.body).then((r) => setCopied({ ...r, what: "payload JSON" }));
2131
+ if ((input === "s" || key.return) && built?.report.ok) doSend();
1404
2132
  });
1405
2133
  useEffect(() => {
2134
+ if (step === STEP.EVENT) return;
1406
2135
  let alive = true;
1407
2136
  fetchCatalogue(config).then((list) => {
1408
2137
  if (!alive) return;
@@ -1421,7 +2150,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1421
2150
  return () => {
1422
2151
  alive = false;
1423
2152
  };
1424
- }, []);
2153
+ }, [step === STEP.EVENT]);
1425
2154
  const advance = (nextItems, nextCursor) => {
1426
2155
  if (nextCursor >= chosen.length) {
1427
2156
  setItems(nextItems);
@@ -1465,7 +2194,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1465
2194
  const onDiscount = (raw) => {
1466
2195
  const discount = Math.max(0, Number.parseFloat(raw) || 0);
1467
2196
  try {
1468
- const next = draftPayload({ cfg: config, providerId, items, discount });
2197
+ const next = draftPayload({ cfg: config, providerId, items, discount, eventName });
1469
2198
  setDraft({ ...next, discount });
1470
2199
  setStep(STEP.MODE);
1471
2200
  } catch (err) {
@@ -1475,10 +2204,20 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1475
2204
  };
1476
2205
  const finalise = (payload) => {
1477
2206
  setStep(STEP.BUILDING);
1478
- buildPayload2({ cfg: config, providerId, items, discount: draft.discount, payload }).then(async (result) => {
2207
+ buildPayload4({ cfg: config, providerId, items, discount: draft.discount, payload, eventName }).then(async (result) => {
1479
2208
  setBuilt(result);
1480
- if (config.defaults.copyToClipboard !== false) {
1481
- setCopied(await copy(result.body));
2209
+ let req = null;
2210
+ let command = "";
2211
+ try {
2212
+ req = buildRequest({ provider: result.provider, config, body: result.body });
2213
+ command = toCurl({ url: req.url, headers: req.headers, body: result.body });
2214
+ } catch {
2215
+ req = null;
2216
+ }
2217
+ setRequest(req);
2218
+ setCurl(command);
2219
+ if (config.defaults.copyToClipboard !== false && command) {
2220
+ setCopied({ ...await copy(command), what: "curl" });
1482
2221
  }
1483
2222
  setStep(STEP.REPORT);
1484
2223
  }).catch((err) => {
@@ -1514,22 +2253,37 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1514
2253
  };
1515
2254
  const store = config.shopify.domain;
1516
2255
  const currency = config.defaults.currency;
1517
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1518
- /* @__PURE__ */ jsx7(
2256
+ const coverageMissing = built ? built.report.coverage.filter((c2) => c2.status === "MISSING").length : 0;
2257
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2258
+ /* @__PURE__ */ jsx8(
1519
2259
  Header,
1520
2260
  {
1521
2261
  title: "Build payload",
1522
- subtitle: `${metaFor(providerId).label} \xB7 abandoned checkout`,
2262
+ subtitle: `${metaFor(providerId).label} \xB7 ${multiEvent ? eventName : "abandoned checkout"}`,
1523
2263
  right: store
1524
2264
  }
1525
2265
  ),
1526
- step === STEP.LOADING && /* @__PURE__ */ jsx7(Spinner, { label: `fetching live products from ${store}\u2026` }),
1527
- step === STEP.PICK && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1528
- /* @__PURE__ */ jsxs7(Text7, { children: [
2266
+ step === STEP.EVENT && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2267
+ /* @__PURE__ */ jsx8(Text8, { children: "Which event do you want to send?" }),
2268
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
2269
+ Select4,
2270
+ {
2271
+ visibleOptionCount: provider.events.length,
2272
+ options: provider.events.map((e) => ({ label: `${e.label.padEnd(20)} ${e.eventName}`, value: e.eventName })),
2273
+ onChange: (value) => {
2274
+ setEventName(value);
2275
+ setStep(STEP.LOADING);
2276
+ }
2277
+ }
2278
+ ) })
2279
+ ] }),
2280
+ step === STEP.LOADING && /* @__PURE__ */ jsx8(Spinner, { label: `fetching live products from ${store}\u2026` }),
2281
+ step === STEP.PICK && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2282
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1529
2283
  `Select the products in the abandoned cart `,
1530
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "(space toggles \xB7 enter confirms)" })
2284
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "(space toggles \xB7 enter confirms)" })
1531
2285
  ] }),
1532
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
2286
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
1533
2287
  MultiSelect,
1534
2288
  {
1535
2289
  visibleOptionCount: 10,
@@ -1541,12 +2295,12 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1541
2295
  }
1542
2296
  ) })
1543
2297
  ] }),
1544
- step === STEP.VARIANT && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1545
- /* @__PURE__ */ jsxs7(Text7, { children: [
2298
+ step === STEP.VARIANT && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2299
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1546
2300
  `Variant for `,
1547
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: palette.accent, children: chosen[cursor]?.title })
2301
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: palette.accent, children: chosen[cursor]?.title })
1548
2302
  ] }),
1549
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
2303
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
1550
2304
  Select4,
1551
2305
  {
1552
2306
  visibleOptionCount: 8,
@@ -1559,33 +2313,33 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1559
2313
  `variant-${cursor}`
1560
2314
  ) })
1561
2315
  ] }),
1562
- step === STEP.QUANTITY && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1563
- /* @__PURE__ */ jsxs7(Text7, { children: [
2316
+ step === STEP.QUANTITY && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2317
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1564
2318
  `Quantity for `,
1565
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: palette.accent, children: chosen[cursor]?.title }),
1566
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: pendingVariant?.title ? ` \xB7 ${pendingVariant.title}` : "" })
2319
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: palette.accent, children: chosen[cursor]?.title }),
2320
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: pendingVariant?.title ? ` \xB7 ${pendingVariant.title}` : "" })
1567
2321
  ] }),
1568
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1569
- /* @__PURE__ */ jsx7(Text7, { color: palette.accent, children: "\u276F " }),
1570
- /* @__PURE__ */ jsx7(TextInput, { defaultValue: "1", placeholder: "1", onSubmit: onQuantity }, `qty-${cursor}`)
2322
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
2323
+ /* @__PURE__ */ jsx8(Text8, { color: palette.accent, children: "\u276F " }),
2324
+ /* @__PURE__ */ jsx8(TextInput, { defaultValue: "1", placeholder: "1", onSubmit: onQuantity }, `qty-${cursor}`)
1571
2325
  ] })
1572
2326
  ] }),
1573
- step === STEP.DISCOUNT && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1574
- /* @__PURE__ */ jsxs7(Text7, { children: [
2327
+ step === STEP.DISCOUNT && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2328
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1575
2329
  `Cart discount in ${currency} `,
1576
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "(0 for none)" })
2330
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "(0 for none)" })
1577
2331
  ] }),
1578
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1579
- /* @__PURE__ */ jsx7(Text7, { color: palette.accent, children: "\u276F " }),
1580
- /* @__PURE__ */ jsx7(TextInput, { defaultValue: "0", placeholder: "0", onSubmit: onDiscount })
2332
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
2333
+ /* @__PURE__ */ jsx8(Text8, { color: palette.accent, children: "\u276F " }),
2334
+ /* @__PURE__ */ jsx8(TextInput, { defaultValue: "0", placeholder: "0", onSubmit: onDiscount })
1581
2335
  ] })
1582
2336
  ] }),
1583
- step === STEP.MODE && draft && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1584
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: palette.accent, paddingX: 1, marginBottom: 1, children: [
1585
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: `${draft.fields.length} fields have been filled in from Shopify and your profile.` }),
1586
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "Review them one by one, or accept the suggestions as they are." })
2337
+ step === STEP.MODE && draft && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2338
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: palette.accent, paddingX: 1, marginBottom: 1, children: [
2339
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: `${draft.fields.length} fields have been filled in from Shopify and your profile.` }),
2340
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "Review them one by one, or accept the suggestions as they are." })
1587
2341
  ] }),
1588
- /* @__PURE__ */ jsx7(
2342
+ /* @__PURE__ */ jsx8(
1589
2343
  Select4,
1590
2344
  {
1591
2345
  options: [
@@ -1596,7 +2350,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1596
2350
  }
1597
2351
  )
1598
2352
  ] }),
1599
- step === STEP.FIELDS && draft && /* @__PURE__ */ jsx7(
2353
+ step === STEP.FIELDS && draft && /* @__PURE__ */ jsx8(
1600
2354
  FieldPrompt,
1601
2355
  {
1602
2356
  field: draft.fields[fieldIndex],
@@ -1606,37 +2360,79 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1606
2360
  onSubmit: onFieldSubmit
1607
2361
  }
1608
2362
  ),
1609
- step === STEP.BUILDING && /* @__PURE__ */ jsx7(Spinner, { label: "assembling payload, verifying images and signature fidelity\u2026" }),
1610
- (step === STEP.REPORT || step === STEP.CONFIRM) && built && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1611
- /* @__PURE__ */ jsx7(Summary, { built, currency }),
1612
- /* @__PURE__ */ jsx7(Section, { title: "Pre-flight", children: /* @__PURE__ */ jsx7(CheckList, { checks: built.report.checks }) }),
1613
- /* @__PURE__ */ jsx7(Section, { title: "Field coverage", children: /* @__PURE__ */ jsx7(FieldTable, { coverage: built.report.coverage }) }),
1614
- /* @__PURE__ */ jsx7(Section, { title: "Request", children: /* @__PURE__ */ jsx7(RequestPreview, { config, built }) }),
1615
- showPayload ? /* @__PURE__ */ jsx7(Section, { title: "Payload", children: /* @__PURE__ */ jsx7(Box7, { borderStyle: "round", borderColor: palette.dim, paddingX: 1, flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: JSON.stringify(built.payload, null, 2) }) }) }) : null,
1616
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: copied?.ok ? /* @__PURE__ */ jsx7(Text7, { color: palette.ok, children: `\u2714 payload copied to clipboard via ${copied.via}` }) : /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: `payload saved to ${built.file} \xB7 press c to copy` }) }),
1617
- /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "p toggles the full payload \xB7 c copies it" }) }),
1618
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: built.report.ok ? /* @__PURE__ */ jsx7(Alert, { variant: "success", children: "payload is valid \u2014 the event will pass the schema gate and signature check" }) : /* @__PURE__ */ jsx7(Alert, { variant: "error", children: `blocking: ${built.report.blocking.map((b) => b.name).join(", ")}` }) }),
1619
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1620
- /* @__PURE__ */ jsx7(Text7, { children: "Send it to the endpoint now? " }),
1621
- /* @__PURE__ */ jsx7(
1622
- ConfirmInput,
2363
+ step === STEP.BUILDING && /* @__PURE__ */ jsx8(Spinner, { label: "assembling payload, verifying images and signature fidelity\u2026" }),
2364
+ (step === STEP.REPORT || step === STEP.CONFIRM) && built && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2365
+ /* @__PURE__ */ jsx8(Summary, { built, currency }),
2366
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
2367
+ /* @__PURE__ */ jsx8(
2368
+ StatusRow,
2369
+ {
2370
+ label: "Pre-flight",
2371
+ ok: built.report.ok,
2372
+ value: built.report.ok ? `${built.report.checks.length} checks passed` : `${built.report.blocking.length} blocking`,
2373
+ detail: built.report.ok ? "" : built.report.blocking.map((b) => b.name).join(", ")
2374
+ }
2375
+ ),
2376
+ /* @__PURE__ */ jsx8(
2377
+ StatusRow,
2378
+ {
2379
+ label: "Coverage",
2380
+ ok: coverageMissing === 0,
2381
+ value: `${built.report.coverage.length - coverageMissing} of ${built.report.coverage.length} fields resolved`,
2382
+ detail: coverageMissing ? "missing fields render blank in templates" : ""
2383
+ }
2384
+ ),
2385
+ /* @__PURE__ */ jsx8(
2386
+ StatusRow,
2387
+ {
2388
+ label: "Request",
2389
+ ok: Boolean(request),
2390
+ value: request ? `POST \xB7 ${built.body.length} bytes` : "no destination configured",
2391
+ detail: request ? truncate(request.url, 44) : "Setup \u2192 Webhook"
2392
+ }
2393
+ ),
2394
+ /* @__PURE__ */ jsx8(
2395
+ StatusRow,
1623
2396
  {
1624
- isDisabled: !built.report.ok,
1625
- onConfirm: doSend,
1626
- onCancel: () => onDone()
2397
+ label: "Clipboard",
2398
+ ok: copied?.ok,
2399
+ warn: !copied?.ok,
2400
+ value: copied?.ok ? `${copied.what} copied` : "not copied",
2401
+ detail: copied?.ok ? `via ${copied.via}` : "press c"
1627
2402
  }
1628
2403
  )
1629
2404
  ] }),
1630
- !built.report.ok && /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: " sending is disabled while pre-flight fails \u2014 fix the payload, or use the CLI with --force" })
2405
+ panel === "checks" && /* @__PURE__ */ jsx8(Panel, { title: "Pre-flight checks", children: /* @__PURE__ */ jsx8(CheckList, { checks: built.report.checks }) }),
2406
+ panel === "fields" && /* @__PURE__ */ jsx8(Panel, { title: "Field coverage", hint: "what the receiver will read", children: /* @__PURE__ */ jsx8(FieldTable, { coverage: built.report.coverage }) }),
2407
+ panel === "request" && /* @__PURE__ */ jsx8(Panel, { title: "Request", children: /* @__PURE__ */ jsx8(RequestPreview, { request, bytes: built.body.length }) }),
2408
+ panel === "curl" && curl && /* @__PURE__ */ jsx8(Panel, { title: "curl", hint: "Postman \u2192 Import \u2192 Raw text", children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: curl }) }),
2409
+ panel === "payload" && /* @__PURE__ */ jsx8(Panel, { title: "Payload", hint: `${built.body.length} bytes`, children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: JSON.stringify(built.payload, null, 2) }) }),
2410
+ !built.report.ok && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Alert, { variant: "error", children: `cannot send \u2014 ${built.report.blocking.map((b) => b.detail).join("; ")}` }) }),
2411
+ /* @__PURE__ */ jsx8(
2412
+ CommandBar,
2413
+ {
2414
+ commands: [
2415
+ { key: "s", label: "send", disabled: !built.report.ok },
2416
+ { key: "c", label: "copy curl" },
2417
+ { key: "j", label: "copy json" },
2418
+ { key: "u", label: "curl", active: panel === "curl" },
2419
+ { key: "p", label: "payload", active: panel === "payload" },
2420
+ { key: "f", label: "fields", active: panel === "fields" },
2421
+ { key: "v", label: "checks", active: panel === "checks" },
2422
+ { key: "r", label: "request", active: panel === "request" },
2423
+ { key: "esc", label: "back" }
2424
+ ]
2425
+ }
2426
+ )
1631
2427
  ] }),
1632
- step === STEP.SENDING && /* @__PURE__ */ jsx7(Spinner, { label: "signing and POSTing the webhook\u2026" }),
1633
- step === STEP.RESULT && sendResult && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1634
- /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
1635
- sendResult.ok ? /* @__PURE__ */ jsx7(Badge, { color: "green", children: `HTTP ${sendResult.status}` }) : /* @__PURE__ */ jsx7(Badge, { color: "red", children: sendResult.error ? "FAILED" : `HTTP ${sendResult.status}` }),
1636
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
1637
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: sendResult.error ?? explainStatus(sendResult.status) })
2428
+ step === STEP.SENDING && /* @__PURE__ */ jsx8(Spinner, { label: "signing and POSTing the webhook\u2026" }),
2429
+ step === STEP.RESULT && sendResult && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2430
+ /* @__PURE__ */ jsxs8(Box8, { marginBottom: 1, children: [
2431
+ sendResult.ok ? /* @__PURE__ */ jsx8(Badge, { color: "green", children: `HTTP ${sendResult.status}` }) : /* @__PURE__ */ jsx8(Badge, { color: "red", children: sendResult.error ? "FAILED" : `HTTP ${sendResult.status}` }),
2432
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2433
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: sendResult.error ?? explainStatus(sendResult.status) })
1638
2434
  ] }),
1639
- /* @__PURE__ */ jsx7(
2435
+ /* @__PURE__ */ jsx8(
1640
2436
  KeyValue,
1641
2437
  {
1642
2438
  rows: [
@@ -1649,28 +2445,22 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
1649
2445
  ]
1650
2446
  }
1651
2447
  ),
1652
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1653
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "reproduce:" }),
1654
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: sendResult.curl })
2448
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
2449
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "reproduce:" }),
2450
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: sendResult.curl })
1655
2451
  ] }),
1656
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "press enter to return to the menu" }) })
2452
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "press enter to return to the menu" }) })
1657
2453
  ] }),
1658
- step === STEP.ERROR && error && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1659
- /* @__PURE__ */ jsx7(Alert, { variant: "error", children: error.message }),
1660
- error.hint ? /* @__PURE__ */ jsx7(Text7, { color: palette.warn, children: ` ${error.hint}` }) : null,
1661
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "press enter to return to the menu" }) })
2454
+ step === STEP.ERROR && error && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2455
+ /* @__PURE__ */ jsx8(Alert, { variant: "error", children: error.message }),
2456
+ error.hint ? /* @__PURE__ */ jsx8(Text8, { color: palette.warn, children: ` ${error.hint}` }) : null,
2457
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "press enter to return to the menu" }) })
1662
2458
  ] })
1663
2459
  ] });
1664
2460
  }
1665
- function RequestPreview({ config, built }) {
1666
- let request = null;
1667
- try {
1668
- request = buildRequest({ provider: built.provider, config, body: built.body });
1669
- } catch {
1670
- request = null;
1671
- }
1672
- if (!request) return /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: " destination not configured" });
1673
- return /* @__PURE__ */ jsx7(
2461
+ function RequestPreview({ request, bytes: bytes2 }) {
2462
+ if (!request) return /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: " destination not configured" });
2463
+ return /* @__PURE__ */ jsx8(
1674
2464
  KeyValue,
1675
2465
  {
1676
2466
  keyWidth: 22,
@@ -1678,27 +2468,27 @@ function RequestPreview({ config, built }) {
1678
2468
  ["POST", truncate(request.url, 56)],
1679
2469
  ["x-webhook-timestamp", request.timestamp],
1680
2470
  ["x-webhook-signature", truncate(request.signature, 46)],
1681
- ["body", `${built.body.length} bytes`]
2471
+ ["body", `${bytes2} bytes`]
1682
2472
  ]
1683
2473
  }
1684
2474
  );
1685
2475
  }
1686
2476
  function FieldPrompt({ field, index, total, value, onSubmit }) {
1687
2477
  const filled = Math.round(index / total * 30);
1688
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1689
- /* @__PURE__ */ jsxs7(Box7, { children: [
1690
- /* @__PURE__ */ jsx7(Text7, { color: palette.accent, children: "\u2588".repeat(filled) }),
1691
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "\u2591".repeat(30 - filled) }),
1692
- /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: ` ${index + 1}/${total} ${field.group}` })
2478
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2479
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2480
+ /* @__PURE__ */ jsx8(Text8, { color: palette.accent, children: "\u2588".repeat(filled) }),
2481
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "\u2591".repeat(30 - filled) }),
2482
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: ` ${index + 1}/${total} ${field.group}` })
1693
2483
  ] }),
1694
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1695
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: palette.heading, children: field.label }),
1696
- field.hint ? /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: ` ${field.hint}` }) : null,
1697
- field.optional ? /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: " optional" }) : null
2484
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
2485
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: palette.heading, children: field.label }),
2486
+ field.hint ? /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: ` ${field.hint}` }) : null,
2487
+ field.optional ? /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: " optional" }) : null
1698
2488
  ] }),
1699
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1700
- /* @__PURE__ */ jsx7(Text7, { color: palette.accent, children: "\u276F " }),
1701
- /* @__PURE__ */ jsx7(
2489
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
2490
+ /* @__PURE__ */ jsx8(Text8, { color: palette.accent, children: "\u276F " }),
2491
+ /* @__PURE__ */ jsx8(
1702
2492
  TextInput,
1703
2493
  {
1704
2494
  defaultValue: value == null ? "" : String(value),
@@ -1708,32 +2498,29 @@ function FieldPrompt({ field, index, total, value, onSubmit }) {
1708
2498
  field.path
1709
2499
  )
1710
2500
  ] }),
1711
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "enter accepts the suggestion \xB7 edit the text to change it" }) })
2501
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "enter accepts the suggestion \xB7 edit the text to change it \xB7 esc goes back a field" }) })
1712
2502
  ] });
1713
2503
  }
1714
2504
  function Summary({ built, currency }) {
1715
- const d = built.payload.data;
1716
- return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx7(
1717
- KeyValue,
1718
- {
1719
- rows: [
1720
- ["type", built.payload.type],
1721
- ["store", d.store_url],
1722
- ["customer", `${d.customer.shipping_address.customer_name} \xB7 ${d.phone}`],
1723
- ["items", d.line_items.map((i) => `${truncate(i.name, 28)} \xD7${i.quantity}`).join(", ")],
1724
- ["total", `${money(d.total_price, d.line_items[0]?.currency ?? currency)} (was ${money(d.original_total_price)} \xB7 discount ${money(d.total_discount)})`],
1725
- ["checkout", truncate(d.abandoned_checkout_url, 58)],
1726
- ["saved", built.file]
1727
- ]
1728
- }
1729
- ) });
2505
+ const s = built.summary ?? (built.provider.summarize ? built.provider.summarize(built.payload) : {});
2506
+ const rows = [
2507
+ ["provider", built.provider.label],
2508
+ ["event", s.event ?? "\u2014"],
2509
+ ["store", s.store ?? "\u2014"],
2510
+ ["customer", s.customer || "\u2014"],
2511
+ ["items", s.items || "\u2014"],
2512
+ ["total", s.total == null ? "\u2014" : `${money(s.total, s.currency ?? currency)}${s.extra ? ` (${s.extra})` : ""}`]
2513
+ ];
2514
+ if (s.link) rows.push(["link", truncate(s.link, 58)]);
2515
+ rows.push(["saved", built.file]);
2516
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx8(KeyValue, { rows }) });
1730
2517
  }
1731
2518
 
1732
2519
  // src/ui/screens/Configure.jsx
1733
- import React8, { useState as useState2 } from "react";
1734
- import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
2520
+ import React9, { useState as useState2 } from "react";
2521
+ import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
1735
2522
  import { Spinner as Spinner2, TextInput as TextInput2, PasswordInput, Select as Select5, Alert as Alert2 } from "@inkjs/ui";
1736
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2523
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1737
2524
  var FIELDS = [
1738
2525
  { group: "Customer", key: "customer.firstName", label: "First name", placeholder: "Test" },
1739
2526
  { group: "Customer", key: "customer.lastName", label: "Last name", placeholder: "Shopper" },
@@ -1808,9 +2595,9 @@ function Configure({ config, section: initialSection, onDone }) {
1808
2595
  });
1809
2596
  };
1810
2597
  if (!section) {
1811
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1812
- /* @__PURE__ */ jsx8(Header, { title: "Configure", subtitle: "pick what you want to change", right: "esc to go back" }),
1813
- /* @__PURE__ */ jsx8(
2598
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2599
+ /* @__PURE__ */ jsx9(Header, { title: "Configure", subtitle: "pick what you want to change", right: "esc to go back" }),
2600
+ /* @__PURE__ */ jsx9(
1814
2601
  Select5,
1815
2602
  {
1816
2603
  visibleOptionCount: 8,
@@ -1827,29 +2614,29 @@ function Configure({ config, section: initialSection, onDone }) {
1827
2614
  ] });
1828
2615
  }
1829
2616
  if (phase === "verifying") {
1830
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1831
- /* @__PURE__ */ jsx8(Header, { title: "Configure", subtitle: "verifying credentials" }),
1832
- /* @__PURE__ */ jsx8(Spinner2, { label: "contacting Shopify\u2026" })
2617
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2618
+ /* @__PURE__ */ jsx9(Header, { title: "Configure", subtitle: "verifying credentials" }),
2619
+ /* @__PURE__ */ jsx9(Spinner2, { label: "contacting Shopify\u2026" })
1833
2620
  ] });
1834
2621
  }
1835
2622
  if (phase === "done") {
1836
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1837
- /* @__PURE__ */ jsx8(Header, { title: "Configure", subtitle: "saved" }),
1838
- /* @__PURE__ */ jsx8(Alert2, { variant: verifyMsg?.ok ? "success" : "warning", children: verifyMsg?.text ?? "saved" }),
1839
- verifyMsg?.hint ? /* @__PURE__ */ jsx8(Text8, { color: palette.warn, children: ` ${verifyMsg.hint}` }) : null,
1840
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: `config written to ${verifyMsg?.file} (chmod 600)` }) }),
1841
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "press enter to return to the menu" }) })
2623
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2624
+ /* @__PURE__ */ jsx9(Header, { title: "Configure", subtitle: "saved" }),
2625
+ /* @__PURE__ */ jsx9(Alert2, { variant: verifyMsg?.ok ? "success" : "warning", children: verifyMsg?.text ?? "saved" }),
2626
+ verifyMsg?.hint ? /* @__PURE__ */ jsx9(Text9, { color: palette.warn, children: ` ${verifyMsg.hint}` }) : null,
2627
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: `config written to ${verifyMsg?.file} (chmod 600)` }) }),
2628
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: "press enter to return to the menu" }) })
1842
2629
  ] });
1843
2630
  }
1844
2631
  const current = get(draft, field.key) ?? "";
1845
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1846
- /* @__PURE__ */ jsx8(Header, { title: "Configure", subtitle: `${field.group} \xB7 step ${index + 1} of ${fields.length}`, right: "esc to cancel" }),
1847
- /* @__PURE__ */ jsx8(Progress, { index, total: fields.length }),
1848
- /* @__PURE__ */ jsxs8(Section, { title: field.label, children: [
1849
- field.hint ? /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: ` ${field.hint}` }) : null,
1850
- /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
1851
- /* @__PURE__ */ jsx8(Text8, { color: palette.accent, children: `${glyph.pointer} ` }),
1852
- field.type === "select" ? /* @__PURE__ */ jsx8(
2632
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2633
+ /* @__PURE__ */ jsx9(Header, { title: "Configure", subtitle: `${field.group} \xB7 step ${index + 1} of ${fields.length}`, right: "esc to cancel" }),
2634
+ /* @__PURE__ */ jsx9(Progress, { index, total: fields.length }),
2635
+ /* @__PURE__ */ jsxs9(Section, { title: field.label, children: [
2636
+ field.hint ? /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: ` ${field.hint}` }) : null,
2637
+ /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
2638
+ /* @__PURE__ */ jsx9(Text9, { color: palette.accent, children: `${glyph.pointer} ` }),
2639
+ field.type === "select" ? /* @__PURE__ */ jsx9(
1853
2640
  Select5,
1854
2641
  {
1855
2642
  options: field.options,
@@ -1857,7 +2644,7 @@ function Configure({ config, section: initialSection, onDone }) {
1857
2644
  onChange: commit
1858
2645
  },
1859
2646
  field.key
1860
- ) : field.secret ? /* @__PURE__ */ jsx8(PasswordInput, { placeholder: current ? mask(current) : "\u2022\u2022\u2022\u2022\u2022\u2022", onSubmit: (v) => commit(v || current) }, field.key) : /* @__PURE__ */ jsx8(
2647
+ ) : field.secret ? /* @__PURE__ */ jsx9(PasswordInput, { placeholder: current ? mask(current) : "\u2022\u2022\u2022\u2022\u2022\u2022", onSubmit: (v) => commit(v || current) }, field.key) : /* @__PURE__ */ jsx9(
1861
2648
  TextInput2,
1862
2649
  {
1863
2650
  defaultValue: current,
@@ -1872,18 +2659,18 @@ function Configure({ config, section: initialSection, onDone }) {
1872
2659
  }
1873
2660
  function Progress({ index, total }) {
1874
2661
  const filled = Math.round(index / total * 30);
1875
- return /* @__PURE__ */ jsxs8(Box8, { children: [
1876
- /* @__PURE__ */ jsx8(Text8, { color: palette.accent, children: "\u2588".repeat(filled) }),
1877
- /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "\u2591".repeat(30 - filled) }),
1878
- /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: ` ${index}/${total}` })
2662
+ return /* @__PURE__ */ jsxs9(Box9, { children: [
2663
+ /* @__PURE__ */ jsx9(Text9, { color: palette.accent, children: "\u2588".repeat(filled) }),
2664
+ /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: "\u2591".repeat(30 - filled) }),
2665
+ /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: ` ${index}/${total}` })
1879
2666
  ] });
1880
2667
  }
1881
2668
 
1882
2669
  // src/ui/screens/ConnectShopify.jsx
1883
- import React9, { useState as useState3 } from "react";
1884
- import { Box as Box9, Text as Text9, useInput as useInput5 } from "ink";
2670
+ import React10, { useState as useState3 } from "react";
2671
+ import { Box as Box10, Text as Text10, useInput as useInput5 } from "ink";
1885
2672
  import { Spinner as Spinner3, TextInput as TextInput3, PasswordInput as PasswordInput2, Alert as Alert3, Badge as Badge2 } from "@inkjs/ui";
1886
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2673
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1887
2674
  var STEP2 = { DOMAIN: "domain", TOKEN: "token", VERIFY: "verify", DONE: "done", FAILED: "failed" };
1888
2675
  function ConnectShopify({ config, onDone }) {
1889
2676
  const [draft] = useState3(() => structuredClone(config));
@@ -1914,13 +2701,13 @@ function ConnectShopify({ config, onDone }) {
1914
2701
  setStep(STEP2.FAILED);
1915
2702
  });
1916
2703
  };
1917
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1918
- /* @__PURE__ */ jsx9(Header, { title: "Connect Shopify", subtitle: "the catalogue every payload is built from", right: "esc to cancel" }),
1919
- step === STEP2.DOMAIN && /* @__PURE__ */ jsxs9(Section, { title: "Store domain", children: [
1920
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " the myshopify.com domain, not your custom domain" }),
1921
- /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
1922
- /* @__PURE__ */ jsx9(Text9, { color: palette.accent, children: `${glyph.pointer} ` }),
1923
- /* @__PURE__ */ jsx9(
2704
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2705
+ /* @__PURE__ */ jsx10(Header, { title: "Connect Shopify", subtitle: "the catalogue every payload is built from", right: "esc to cancel" }),
2706
+ step === STEP2.DOMAIN && /* @__PURE__ */ jsxs10(Section, { title: "Store domain", children: [
2707
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " the myshopify.com domain, not your custom domain" }),
2708
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2709
+ /* @__PURE__ */ jsx10(Text10, { color: palette.accent, children: `${glyph.pointer} ` }),
2710
+ /* @__PURE__ */ jsx10(
1924
2711
  TextInput3,
1925
2712
  {
1926
2713
  defaultValue: draft.shopify.domain,
@@ -1930,12 +2717,12 @@ function ConnectShopify({ config, onDone }) {
1930
2717
  )
1931
2718
  ] })
1932
2719
  ] }),
1933
- step === STEP2.TOKEN && /* @__PURE__ */ jsxs9(Section, { title: "Admin API access token", children: [
1934
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " Shopify admin \u2192 Settings \u2192 Apps and sales channels \u2192 Develop apps" }),
1935
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " the token needs the read_products scope" }),
1936
- /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
1937
- /* @__PURE__ */ jsx9(Text9, { color: palette.accent, children: `${glyph.pointer} ` }),
1938
- /* @__PURE__ */ jsx9(
2720
+ step === STEP2.TOKEN && /* @__PURE__ */ jsxs10(Section, { title: "Admin API access token", children: [
2721
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " Shopify admin \u2192 Settings \u2192 Apps and sales channels \u2192 Develop apps" }),
2722
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " the token needs the read_products scope" }),
2723
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2724
+ /* @__PURE__ */ jsx10(Text10, { color: palette.accent, children: `${glyph.pointer} ` }),
2725
+ /* @__PURE__ */ jsx10(
1939
2726
  PasswordInput2,
1940
2727
  {
1941
2728
  placeholder: draft.shopify.accessToken ? mask(draft.shopify.accessToken) : "shpat_\u2026",
@@ -1944,14 +2731,14 @@ function ConnectShopify({ config, onDone }) {
1944
2731
  )
1945
2732
  ] })
1946
2733
  ] }),
1947
- step === STEP2.VERIFY && /* @__PURE__ */ jsx9(Spinner3, { label: `connecting to ${draft.shopify.domain}\u2026` }),
1948
- step === STEP2.DONE && result && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1949
- /* @__PURE__ */ jsxs9(Box9, { marginBottom: 1, children: [
1950
- /* @__PURE__ */ jsx9(Badge2, { color: "green", children: "CONNECTED" }),
1951
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
1952
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: result.shop?.name })
2734
+ step === STEP2.VERIFY && /* @__PURE__ */ jsx10(Spinner3, { label: `connecting to ${draft.shopify.domain}\u2026` }),
2735
+ step === STEP2.DONE && result && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2736
+ /* @__PURE__ */ jsxs10(Box10, { marginBottom: 1, children: [
2737
+ /* @__PURE__ */ jsx10(Badge2, { color: "green", children: "CONNECTED" }),
2738
+ /* @__PURE__ */ jsx10(Text10, { children: " " }),
2739
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: result.shop?.name })
1953
2740
  ] }),
1954
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx9(
2741
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx10(
1955
2742
  KeyValue,
1956
2743
  {
1957
2744
  keyWidth: 16,
@@ -1967,29 +2754,29 @@ function ConnectShopify({ config, onDone }) {
1967
2754
  ]
1968
2755
  }
1969
2756
  ) }),
1970
- result.applied.length ? /* @__PURE__ */ jsx9(Section, { title: "Picked up from the store", children: result.applied.map((line, i) => /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: ` ${glyph.tick} ${line}` }, i)) }) : null,
1971
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Alert3, { variant: "success", children: "Shopify connected \u2014 you can browse the real catalogue now" }) }),
1972
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: "press enter to continue" }) })
2757
+ result.applied.length ? /* @__PURE__ */ jsx10(Section, { title: "Picked up from the store", children: result.applied.map((line, i) => /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: ` ${glyph.tick} ${line}` }, i)) }) : null,
2758
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Alert3, { variant: "success", children: "Shopify connected \u2014 you can browse the real catalogue now" }) }),
2759
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: "press enter to continue" }) })
1973
2760
  ] }),
1974
- step === STEP2.FAILED && error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1975
- /* @__PURE__ */ jsx9(Alert3, { variant: "error", children: error.message }),
1976
- error.hint ? /* @__PURE__ */ jsx9(Text9, { color: palette.warn, children: ` ${error.hint}` }) : null,
1977
- /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
1978
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " common causes:" }),
1979
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " \xB7 the token is missing the read_products scope" }),
1980
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " \xB7 the token belongs to a different store" }),
1981
- /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: " \xB7 the domain is the custom domain, not the .myshopify.com one" })
2761
+ step === STEP2.FAILED && error && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2762
+ /* @__PURE__ */ jsx10(Alert3, { variant: "error", children: error.message }),
2763
+ error.hint ? /* @__PURE__ */ jsx10(Text10, { color: palette.warn, children: ` ${error.hint}` }) : null,
2764
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexDirection: "column", children: [
2765
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " common causes:" }),
2766
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " \xB7 the token is missing the read_products scope" }),
2767
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " \xB7 the token belongs to a different store" }),
2768
+ /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " \xB7 the domain is the custom domain, not the .myshopify.com one" })
1982
2769
  ] }),
1983
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: "press enter to try again" }) })
2770
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: "press enter to try again" }) })
1984
2771
  ] })
1985
2772
  ] });
1986
2773
  }
1987
2774
 
1988
2775
  // src/ui/screens/Webhook.jsx
1989
- import React10, { useState as useState4 } from "react";
1990
- import { Box as Box10, Text as Text10, useInput as useInput6 } from "ink";
2776
+ import React11, { useState as useState4 } from "react";
2777
+ import { Box as Box11, Text as Text11, useInput as useInput6 } from "ink";
1991
2778
  import { Select as Select6, TextInput as TextInput4, PasswordInput as PasswordInput3, Alert as Alert4, Badge as Badge3, Spinner as Spinner4 } from "@inkjs/ui";
1992
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2779
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1993
2780
  var STEP3 = { PICK: "pick", URL: "url", SECRET: "secret", PROBE: "probe", DONE: "done" };
1994
2781
  function Webhook({ config, onDone }) {
1995
2782
  const [draft] = useState4(() => structuredClone(config));
@@ -2039,9 +2826,9 @@ function Webhook({ config, onDone }) {
2039
2826
  }),
2040
2827
  { label: "Back".padEnd(30), value: "__back" }
2041
2828
  ];
2042
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2043
- /* @__PURE__ */ jsx10(Header, { title: "Webhook destinations", subtitle: "one endpoint and secret per integration", right: "esc to go back" }),
2044
- /* @__PURE__ */ jsx10(
2829
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
2830
+ /* @__PURE__ */ jsx11(Header, { title: "Webhook destinations", subtitle: "one endpoint and secret per integration", right: "esc to go back" }),
2831
+ /* @__PURE__ */ jsx11(
2045
2832
  Select6,
2046
2833
  {
2047
2834
  visibleOptionCount: options.length,
@@ -2054,17 +2841,17 @@ function Webhook({ config, onDone }) {
2054
2841
  }
2055
2842
  }
2056
2843
  ),
2057
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: `${glyph.dot} set globally \u2014 every build for that provider uses it` }) })
2844
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: `${glyph.dot} set globally \u2014 every build for that provider uses it` }) })
2058
2845
  ] });
2059
2846
  }
2060
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2061
- /* @__PURE__ */ jsx10(Header, { title: meta.label, subtitle: `signature: ${meta.signature}`, right: "esc to cancel" }),
2062
- step === STEP3.URL && /* @__PURE__ */ jsxs10(Section, { title: "Webhook URL", children: [
2063
- /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " the full endpoint the provider would call \u2014 paste it as-is" }),
2064
- urlError ? /* @__PURE__ */ jsx10(Text10, { color: palette.bad, children: ` ${glyph.cross} ${urlError}` }) : null,
2065
- /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2066
- /* @__PURE__ */ jsx10(Text10, { color: palette.accent, children: `${glyph.pointer} ` }),
2067
- /* @__PURE__ */ jsx10(
2847
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
2848
+ /* @__PURE__ */ jsx11(Header, { title: meta.label, subtitle: `signature: ${meta.signature}`, right: "esc to cancel" }),
2849
+ step === STEP3.URL && /* @__PURE__ */ jsxs11(Section, { title: "Webhook URL", children: [
2850
+ /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: " the full endpoint the provider would call \u2014 paste it as-is" }),
2851
+ urlError ? /* @__PURE__ */ jsx11(Text11, { color: palette.bad, children: ` ${glyph.cross} ${urlError}` }) : null,
2852
+ /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
2853
+ /* @__PURE__ */ jsx11(Text11, { color: palette.accent, children: `${glyph.pointer} ` }),
2854
+ /* @__PURE__ */ jsx11(
2068
2855
  TextInput4,
2069
2856
  {
2070
2857
  defaultValue: target.webhookUrl,
@@ -2074,12 +2861,12 @@ function Webhook({ config, onDone }) {
2074
2861
  )
2075
2862
  ] })
2076
2863
  ] }),
2077
- step === STEP3.SECRET && /* @__PURE__ */ jsxs10(Section, { title: meta.id === "nitro" ? "Bearer token" : "Signing secret", children: [
2078
- /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: ` sent as ${meta.header}` }),
2079
- /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: " must match what the receiver has stored" }),
2080
- /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2081
- /* @__PURE__ */ jsx10(Text10, { color: palette.accent, children: `${glyph.pointer} ` }),
2082
- /* @__PURE__ */ jsx10(
2864
+ step === STEP3.SECRET && /* @__PURE__ */ jsxs11(Section, { title: meta.id === "nitro" ? "Bearer token" : "Signing secret", children: [
2865
+ /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: ` sent as ${meta.header}` }),
2866
+ /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: " must match what the receiver has stored" }),
2867
+ /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
2868
+ /* @__PURE__ */ jsx11(Text11, { color: palette.accent, children: `${glyph.pointer} ` }),
2869
+ /* @__PURE__ */ jsx11(
2083
2870
  PasswordInput3,
2084
2871
  {
2085
2872
  placeholder: target.webhookSecret ? mask(target.webhookSecret) : "the signing secret\u2026",
@@ -2088,14 +2875,14 @@ function Webhook({ config, onDone }) {
2088
2875
  )
2089
2876
  ] })
2090
2877
  ] }),
2091
- step === STEP3.PROBE && /* @__PURE__ */ jsx10(Spinner4, { label: "checking the endpoint is reachable\u2026" }),
2092
- step === STEP3.DONE && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2093
- /* @__PURE__ */ jsxs10(Box10, { marginBottom: 1, children: [
2094
- /* @__PURE__ */ jsx10(Badge3, { color: probe?.reachable ? "green" : "yellow", children: probe?.reachable ? "REACHABLE" : "UNVERIFIED" }),
2095
- /* @__PURE__ */ jsx10(Text10, { children: " " }),
2096
- /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: probe?.reachable ? `responded HTTP ${probe.status} to an unsigned probe` : probe?.error ?? "could not reach it \u2014 saved anyway" })
2878
+ step === STEP3.PROBE && /* @__PURE__ */ jsx11(Spinner4, { label: "checking the endpoint is reachable\u2026" }),
2879
+ step === STEP3.DONE && /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
2880
+ /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, children: [
2881
+ /* @__PURE__ */ jsx11(Badge3, { color: probe?.reachable ? "green" : "yellow", children: probe?.reachable ? "REACHABLE" : "UNVERIFIED" }),
2882
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
2883
+ /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: probe?.reachable ? `responded HTTP ${probe.status} to an unsigned probe` : probe?.error ?? "could not reach it \u2014 saved anyway" })
2097
2884
  ] }),
2098
- /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx10(
2885
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx11(
2099
2886
  KeyValue,
2100
2887
  {
2101
2888
  keyWidth: 10,
@@ -2106,8 +2893,8 @@ function Webhook({ config, onDone }) {
2106
2893
  ]
2107
2894
  }
2108
2895
  ) }),
2109
- !meta.available ? /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Alert4, { variant: "info", children: `saved \u2014 ${meta.label} payload generation is not implemented yet` }) }) : null,
2110
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { color: palette.dim, children: "press enter to configure another" }) })
2896
+ !meta.available ? /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Alert4, { variant: "info", children: `saved \u2014 ${meta.label} payload generation is not implemented yet` }) }) : null,
2897
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: "press enter to configure another" }) })
2111
2898
  ] })
2112
2899
  ] });
2113
2900
  }
@@ -2130,10 +2917,10 @@ async function probeEndpoint(url) {
2130
2917
  }
2131
2918
 
2132
2919
  // src/ui/screens/Doctor.jsx
2133
- import React11, { useEffect as useEffect2, useState as useState5 } from "react";
2134
- import { Box as Box11, Text as Text11, useInput as useInput7 } from "ink";
2920
+ import React12, { useEffect as useEffect2, useState as useState5 } from "react";
2921
+ import { Box as Box12, Text as Text12, useInput as useInput7 } from "ink";
2135
2922
  import { Spinner as Spinner5 } from "@inkjs/ui";
2136
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2923
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2137
2924
  function Doctor({ config, onDone }) {
2138
2925
  const [checks, setChecks] = useState5(null);
2139
2926
  useInput7((input, key) => {
@@ -2146,13 +2933,13 @@ function Doctor({ config, onDone }) {
2146
2933
  alive = false;
2147
2934
  };
2148
2935
  }, []);
2149
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
2150
- /* @__PURE__ */ jsx11(Header, { title: "Doctor", subtitle: "everything the generator depends on" }),
2151
- !checks ? /* @__PURE__ */ jsx11(Spinner5, { label: "running checks\u2026" }) : /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
2152
- /* @__PURE__ */ jsx11(Section, { title: "Configuration", children: /* @__PURE__ */ jsx11(CheckList, { checks: checks.config }) }),
2153
- /* @__PURE__ */ jsx11(Section, { title: "Connectivity", children: /* @__PURE__ */ jsx11(CheckList, { checks: checks.connectivity }) }),
2154
- /* @__PURE__ */ jsx11(Section, { title: "Runtime", children: /* @__PURE__ */ jsx11(CheckList, { checks: checks.runtime }) }),
2155
- /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color: palette.dim, children: "press enter to return to the menu" }) })
2936
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2937
+ /* @__PURE__ */ jsx12(Header, { title: "Doctor", subtitle: "everything the generator depends on" }),
2938
+ !checks ? /* @__PURE__ */ jsx12(Spinner5, { label: "running checks\u2026" }) : /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2939
+ /* @__PURE__ */ jsx12(Section, { title: "Configuration", children: /* @__PURE__ */ jsx12(CheckList, { checks: checks.config }) }),
2940
+ /* @__PURE__ */ jsx12(Section, { title: "Connectivity", children: /* @__PURE__ */ jsx12(CheckList, { checks: checks.connectivity }) }),
2941
+ /* @__PURE__ */ jsx12(Section, { title: "Runtime", children: /* @__PURE__ */ jsx12(CheckList, { checks: checks.runtime }) }),
2942
+ /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text12, { color: palette.dim, children: "press enter to return to the menu" }) })
2156
2943
  ] })
2157
2944
  ] });
2158
2945
  }
@@ -2200,10 +2987,10 @@ async function run(config) {
2200
2987
  }
2201
2988
 
2202
2989
  // src/ui/screens/History.jsx
2203
- import React12, { useState as useState6 } from "react";
2204
- import { Box as Box12, Text as Text12, useInput as useInput8 } from "ink";
2990
+ import React13, { useState as useState6 } from "react";
2991
+ import { Box as Box13, Text as Text13, useInput as useInput8 } from "ink";
2205
2992
  import { Select as Select7, Spinner as Spinner6, Badge as Badge4, Alert as Alert5, ConfirmInput as ConfirmInput2 } from "@inkjs/ui";
2206
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2993
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2207
2994
  function History({ config, onDone }) {
2208
2995
  const [items] = useState6(() => listPayloads(20));
2209
2996
  const [selected, setSelected] = useState6(null);
@@ -2229,14 +3016,14 @@ function History({ config, onDone }) {
2229
3016
  });
2230
3017
  };
2231
3018
  if (!items.length) {
2232
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2233
- /* @__PURE__ */ jsx12(Header, { title: "History", subtitle: "saved payloads" }),
2234
- /* @__PURE__ */ jsx12(Text12, { color: palette.dim, children: "nothing built yet \u2014 press esc to go back" })
3019
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
3020
+ /* @__PURE__ */ jsx13(Header, { title: "History", subtitle: "saved payloads" }),
3021
+ /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: "nothing built yet \u2014 press esc to go back" })
2235
3022
  ] });
2236
3023
  }
2237
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2238
- /* @__PURE__ */ jsx12(Header, { title: "History", subtitle: `${items.length} saved payload${items.length === 1 ? "" : "s"}`, right: "esc to go back" }),
2239
- phase === "list" && /* @__PURE__ */ jsx12(
3024
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
3025
+ /* @__PURE__ */ jsx13(Header, { title: "History", subtitle: `${items.length} saved payload${items.length === 1 ? "" : "s"}`, right: "esc to go back" }),
3026
+ phase === "list" && /* @__PURE__ */ jsx13(
2240
3027
  Select7,
2241
3028
  {
2242
3029
  visibleOptionCount: 10,
@@ -2247,8 +3034,8 @@ function History({ config, onDone }) {
2247
3034
  onChange: onPick
2248
3035
  }
2249
3036
  ),
2250
- phase === "detail" && selected && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2251
- /* @__PURE__ */ jsx12(Section, { title: "Payload", children: /* @__PURE__ */ jsx12(
3037
+ phase === "detail" && selected && /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
3038
+ /* @__PURE__ */ jsx13(Section, { title: "Payload", children: /* @__PURE__ */ jsx13(
2252
3039
  KeyValue,
2253
3040
  {
2254
3041
  rows: [
@@ -2262,31 +3049,31 @@ function History({ config, onDone }) {
2262
3049
  ]
2263
3050
  }
2264
3051
  ) }),
2265
- /* @__PURE__ */ jsxs12(Box12, { marginTop: 1, children: [
2266
- /* @__PURE__ */ jsx12(Text12, { children: "Re-send this payload (it will be re-signed with a fresh timestamp)? " }),
2267
- /* @__PURE__ */ jsx12(ConfirmInput2, { onConfirm: resend, onCancel: () => setPhase("list") })
3052
+ /* @__PURE__ */ jsxs13(Box13, { marginTop: 1, children: [
3053
+ /* @__PURE__ */ jsx13(Text13, { children: "Re-send this payload (it will be re-signed with a fresh timestamp)? " }),
3054
+ /* @__PURE__ */ jsx13(ConfirmInput2, { onConfirm: resend, onCancel: () => setPhase("list") })
2268
3055
  ] })
2269
3056
  ] }),
2270
- phase === "sending" && /* @__PURE__ */ jsx12(Spinner6, { label: "re-signing and sending\u2026" }),
2271
- phase === "result" && result && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2272
- /* @__PURE__ */ jsxs12(Box12, { marginBottom: 1, children: [
2273
- /* @__PURE__ */ jsx12(Badge4, { color: result.ok ? "green" : "red", children: result.error ? "FAILED" : `HTTP ${result.status}` }),
2274
- /* @__PURE__ */ jsx12(Text12, { children: " " }),
2275
- /* @__PURE__ */ jsx12(Text12, { color: palette.dim, children: result.error ?? explainStatus(result.status) })
3057
+ phase === "sending" && /* @__PURE__ */ jsx13(Spinner6, { label: "re-signing and sending\u2026" }),
3058
+ phase === "result" && result && /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
3059
+ /* @__PURE__ */ jsxs13(Box13, { marginBottom: 1, children: [
3060
+ /* @__PURE__ */ jsx13(Badge4, { color: result.ok ? "green" : "red", children: result.error ? "FAILED" : `HTTP ${result.status}` }),
3061
+ /* @__PURE__ */ jsx13(Text13, { children: " " }),
3062
+ /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: result.error ?? explainStatus(result.status) })
2276
3063
  ] }),
2277
- /* @__PURE__ */ jsx12(Text12, { color: palette.dim, children: "press enter to return" })
3064
+ /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: "press enter to return" })
2278
3065
  ] }),
2279
- phase === "error" && error && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2280
- /* @__PURE__ */ jsx12(Alert5, { variant: "error", children: error.message }),
2281
- error.hint ? /* @__PURE__ */ jsx12(Text12, { color: palette.warn, children: ` ${error.hint}` }) : null,
2282
- /* @__PURE__ */ jsx12(Text12, { color: palette.dim, children: "press enter to return" })
3066
+ phase === "error" && error && /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
3067
+ /* @__PURE__ */ jsx13(Alert5, { variant: "error", children: error.message }),
3068
+ error.hint ? /* @__PURE__ */ jsx13(Text13, { color: palette.warn, children: ` ${error.hint}` }) : null,
3069
+ /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: "press enter to return" })
2283
3070
  ] })
2284
3071
  ] });
2285
3072
  }
2286
3073
 
2287
3074
  // src/ui/screens/Clear.jsx
2288
- import React13, { useState as useState7 } from "react";
2289
- import { Box as Box13, Text as Text13, useInput as useInput9 } from "ink";
3075
+ import React14, { useState as useState7 } from "react";
3076
+ import { Box as Box14, Text as Text14, useInput as useInput9 } from "ink";
2290
3077
  import { Select as Select8, ConfirmInput as ConfirmInput3, Alert as Alert6, Badge as Badge5 } from "@inkjs/ui";
2291
3078
 
2292
3079
  // src/core/cleanup.mjs
@@ -2362,7 +3149,7 @@ var TARGETS = {
2362
3149
  };
2363
3150
 
2364
3151
  // src/ui/screens/Clear.jsx
2365
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3152
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2366
3153
  function Clear({ config, onDone }) {
2367
3154
  const [state] = useState7(() => inspect());
2368
3155
  const [choice, setChoice] = useState7(null);
@@ -2376,23 +3163,23 @@ function Clear({ config, onDone }) {
2376
3163
  setResult(outcome);
2377
3164
  };
2378
3165
  if (result) {
2379
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
2380
- /* @__PURE__ */ jsx13(Header, { title: "Clear", subtitle: "done" }),
2381
- /* @__PURE__ */ jsxs13(Box13, { marginBottom: 1, children: [
2382
- /* @__PURE__ */ jsx13(Badge5, { color: "green", children: "CLEARED" }),
2383
- /* @__PURE__ */ jsx13(Text13, { children: " " }),
2384
- /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: choice === "all" ? `${result.payloads} payload(s), ${result.logs} log, ${result.config} config file removed` : `${result.removed} item(s) removed` })
3166
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
3167
+ /* @__PURE__ */ jsx14(Header, { title: "Clear", subtitle: "done" }),
3168
+ /* @__PURE__ */ jsxs14(Box14, { marginBottom: 1, children: [
3169
+ /* @__PURE__ */ jsx14(Badge5, { color: "green", children: "CLEARED" }),
3170
+ /* @__PURE__ */ jsx14(Text14, { children: " " }),
3171
+ /* @__PURE__ */ jsx14(Text14, { color: palette.dim, children: choice === "all" ? `${result.payloads} payload(s), ${result.logs} log, ${result.config} config file removed` : `${result.removed} item(s) removed` })
2385
3172
  ] }),
2386
- choice === "all" ? /* @__PURE__ */ jsx13(Alert6, { variant: "info", children: "back to first-run state \u2014 reconnect Shopify to continue" }) : null,
2387
- /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: "press enter to return" }) })
3173
+ choice === "all" ? /* @__PURE__ */ jsx14(Alert6, { variant: "info", children: "back to first-run state \u2014 reconnect Shopify to continue" }) : null,
3174
+ /* @__PURE__ */ jsx14(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { color: palette.dim, children: "press enter to return" }) })
2388
3175
  ] });
2389
3176
  }
2390
3177
  if (choice) {
2391
3178
  const target = TARGETS[choice];
2392
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
2393
- /* @__PURE__ */ jsx13(Header, { title: "Clear", subtitle: target.label, right: "esc to cancel" }),
2394
- /* @__PURE__ */ jsx13(Alert6, { variant: "warning", children: `This permanently deletes ${target.description}.` }),
2395
- /* @__PURE__ */ jsx13(Section, { title: "Will be removed", children: choice === "all" ? /* @__PURE__ */ jsx13(
3179
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
3180
+ /* @__PURE__ */ jsx14(Header, { title: "Clear", subtitle: target.label, right: "esc to cancel" }),
3181
+ /* @__PURE__ */ jsx14(Alert6, { variant: "warning", children: `This permanently deletes ${target.description}.` }),
3182
+ /* @__PURE__ */ jsx14(Section, { title: "Will be removed", children: choice === "all" ? /* @__PURE__ */ jsx14(
2396
3183
  KeyValue,
2397
3184
  {
2398
3185
  keyWidth: 10,
@@ -2402,22 +3189,22 @@ function Clear({ config, onDone }) {
2402
3189
  ["config", state.config.label]
2403
3190
  ]
2404
3191
  }
2405
- ) : /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: ` ${glyph.dot} ${choice === "payloads" ? state.payloads.label : choice === "logs" ? state.logs.label : target.description}` }) }),
2406
- choice === "all" || choice === "disconnect" ? /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text13, { color: palette.warn, children: " your Shopify access token will have to be entered again" }) }) : null,
2407
- /* @__PURE__ */ jsxs13(Box13, { marginTop: 1, children: [
2408
- /* @__PURE__ */ jsx13(Text13, { children: "Are you sure? " }),
2409
- /* @__PURE__ */ jsx13(ConfirmInput3, { defaultChoice: "cancel", onConfirm: run2, onCancel: () => setChoice(null) })
3192
+ ) : /* @__PURE__ */ jsx14(Text14, { color: palette.dim, children: ` ${glyph.dot} ${choice === "payloads" ? state.payloads.label : choice === "logs" ? state.logs.label : target.description}` }) }),
3193
+ choice === "all" || choice === "disconnect" ? /* @__PURE__ */ jsx14(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { color: palette.warn, children: " your Shopify access token will have to be entered again" }) }) : null,
3194
+ /* @__PURE__ */ jsxs14(Box14, { marginTop: 1, children: [
3195
+ /* @__PURE__ */ jsx14(Text14, { children: "Are you sure? " }),
3196
+ /* @__PURE__ */ jsx14(ConfirmInput3, { defaultChoice: "cancel", onConfirm: run2, onCancel: () => setChoice(null) })
2410
3197
  ] })
2411
3198
  ] });
2412
3199
  }
2413
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
2414
- /* @__PURE__ */ jsx13(Header, { title: "Clear", subtitle: "remove locally cached data", right: "esc to go back" }),
2415
- /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", marginBottom: 1, paddingX: 1, children: [
2416
- /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: `${glyph.dot} ${state.payloads.label}` }),
2417
- /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: `${glyph.dot} ${state.logs.label}` }),
2418
- /* @__PURE__ */ jsx13(Text13, { color: palette.dim, children: `${glyph.dot} ${state.config.label}` })
3200
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
3201
+ /* @__PURE__ */ jsx14(Header, { title: "Clear", subtitle: "remove locally cached data", right: "esc to go back" }),
3202
+ /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", marginBottom: 1, paddingX: 1, children: [
3203
+ /* @__PURE__ */ jsx14(Text14, { color: palette.dim, children: `${glyph.dot} ${state.payloads.label}` }),
3204
+ /* @__PURE__ */ jsx14(Text14, { color: palette.dim, children: `${glyph.dot} ${state.logs.label}` }),
3205
+ /* @__PURE__ */ jsx14(Text14, { color: palette.dim, children: `${glyph.dot} ${state.config.label}` })
2419
3206
  ] }),
2420
- /* @__PURE__ */ jsx13(
3207
+ /* @__PURE__ */ jsx14(
2421
3208
  Select8,
2422
3209
  {
2423
3210
  visibleOptionCount: 8,
@@ -2432,7 +3219,7 @@ function Clear({ config, onDone }) {
2432
3219
  }
2433
3220
 
2434
3221
  // src/ui/App.jsx
2435
- import { jsx as jsx14 } from "react/jsx-runtime";
3222
+ import { jsx as jsx15 } from "react/jsx-runtime";
2436
3223
  function App({ config: initialConfig, initialScreen = "home" }) {
2437
3224
  const { exit } = useApp2();
2438
3225
  const [config, setConfig] = useState8(initialConfig);
@@ -2449,7 +3236,7 @@ function App({ config: initialConfig, initialScreen = "home" }) {
2449
3236
  const toIntegrations = go("integrations");
2450
3237
  switch (screen) {
2451
3238
  case "setup":
2452
- return /* @__PURE__ */ jsx14(
3239
+ return /* @__PURE__ */ jsx15(
2453
3240
  Setup,
2454
3241
  {
2455
3242
  config,
@@ -2466,7 +3253,7 @@ function App({ config: initialConfig, initialScreen = "home" }) {
2466
3253
  }
2467
3254
  );
2468
3255
  case "integrations":
2469
- return /* @__PURE__ */ jsx14(
3256
+ return /* @__PURE__ */ jsx15(
2470
3257
  Integrations,
2471
3258
  {
2472
3259
  config,
@@ -2479,21 +3266,21 @@ function App({ config: initialConfig, initialScreen = "home" }) {
2479
3266
  }
2480
3267
  );
2481
3268
  case "connect":
2482
- return /* @__PURE__ */ jsx14(ConnectShopify, { config, onDone: toSetup });
3269
+ return /* @__PURE__ */ jsx15(ConnectShopify, { config, onDone: toSetup });
2483
3270
  case "webhook":
2484
- return /* @__PURE__ */ jsx14(Webhook, { config, onDone: toSetup });
3271
+ return /* @__PURE__ */ jsx15(Webhook, { config, onDone: toSetup });
2485
3272
  case "configure":
2486
- return /* @__PURE__ */ jsx14(Configure, { config, section, onDone: toSetup });
3273
+ return /* @__PURE__ */ jsx15(Configure, { config, section, onDone: toSetup });
2487
3274
  case "build":
2488
- return /* @__PURE__ */ jsx14(Build, { config, providerId: provider, onDone: toIntegrations });
3275
+ return /* @__PURE__ */ jsx15(Build, { config, providerId: provider, onDone: toIntegrations });
2489
3276
  case "history":
2490
- return /* @__PURE__ */ jsx14(History, { config, onDone: toHome });
3277
+ return /* @__PURE__ */ jsx15(History, { config, onDone: toHome });
2491
3278
  case "doctor":
2492
- return /* @__PURE__ */ jsx14(Doctor, { config, onDone: toHome });
3279
+ return /* @__PURE__ */ jsx15(Doctor, { config, onDone: toHome });
2493
3280
  case "clear":
2494
- return /* @__PURE__ */ jsx14(Clear, { config, onDone: toHome });
3281
+ return /* @__PURE__ */ jsx15(Clear, { config, onDone: toHome });
2495
3282
  default:
2496
- return /* @__PURE__ */ jsx14(Home, { config, onPick: setScreen, onQuit: exit });
3283
+ return /* @__PURE__ */ jsx15(Home, { config, onPick: setScreen, onQuit: exit });
2497
3284
  }
2498
3285
  }
2499
3286
 
@@ -2582,15 +3369,16 @@ async function headless(command, args, config) {
2582
3369
  return 1;
2583
3370
  }
2584
3371
  }
2585
- function requireConfig(config) {
2586
- if (isConfigured(config)) return null;
2587
- const missing = configStatus(config).filter((r) => !r.ok).map((r) => r.key);
3372
+ function requireConfig(config, providerId = "cashfree-occ") {
3373
+ if (isConfigured(config, providerId)) return null;
3374
+ const missing = configStatus(config, providerId).filter((r) => !r.ok).map((r) => r.key);
2588
3375
  log.fail(`configuration incomplete: ${missing.join(", ")}`);
2589
3376
  log.dim(`run \`hookwright configure\` or edit ${config._path}`);
2590
3377
  return 1;
2591
3378
  }
2592
3379
  async function cmdBuild(args, config) {
2593
- const bad = requireConfig(config);
3380
+ const providerId = typeof args.flags.provider === "string" ? args.flags.provider : "cashfree-occ";
3381
+ const bad = requireConfig(config, providerId);
2594
3382
  if (bad) return bad;
2595
3383
  const count = Number.parseInt(args.flags.items ?? "1", 10) || 1;
2596
3384
  const discount = Number.parseFloat(args.flags.discount ?? "0") || 0;
@@ -2604,8 +3392,10 @@ async function cmdBuild(args, config) {
2604
3392
  }
2605
3393
  const items = autoSelect(products, count);
2606
3394
  log.ok(`${items.length} product(s): ${items.map((i) => i.product.title).join(", ")}`);
2607
- const built = await buildPayload2({
3395
+ const built = await buildPayload4({
2608
3396
  cfg: config,
3397
+ providerId,
3398
+ eventName: typeof args.flags.event === "string" ? args.flags.event : void 0,
2609
3399
  items,
2610
3400
  discount,
2611
3401
  phone: typeof args.flags.phone === "string" ? args.flags.phone : void 0,
@@ -2622,8 +3412,18 @@ async function cmdBuild(args, config) {
2622
3412
  }
2623
3413
  }
2624
3414
  if (args.flags.copy !== false && !args.flags.json) {
2625
- const result = await copy(built.body);
2626
- if (result.ok) log.ok(`payload copied to clipboard via ${result.via}`);
3415
+ let clip = built.body;
3416
+ let what = "payload JSON";
3417
+ try {
3418
+ const request = buildRequest({ provider: built.provider, config, body: built.body });
3419
+ clip = toCurl({ url: request.url, headers: request.headers, body: built.body });
3420
+ what = "curl";
3421
+ } catch (err) {
3422
+ log.warn(`${err.message} \u2014 copying the payload JSON instead`);
3423
+ if (err.hint) log.dim(err.hint);
3424
+ }
3425
+ const result = await copy(clip);
3426
+ if (result.ok) log.ok(`${what} copied to clipboard via ${result.via}`);
2627
3427
  else log.dim(`clipboard unavailable \u2014 install xclip, xsel or wl-copy to enable copying`);
2628
3428
  }
2629
3429
  if (!built.report.ok && !args.flags.force) {
@@ -2651,7 +3451,8 @@ async function cmdSend(args, config, built) {
2651
3451
  cfg: config,
2652
3452
  record,
2653
3453
  force: Boolean(args.flags.force),
2654
- dryRun: Boolean(args.flags["dry-run"])
3454
+ dryRun: Boolean(args.flags["dry-run"]),
3455
+ checkEndpoint: args.flags["endpoint-check"] !== false
2655
3456
  });
2656
3457
  if (result.dryRun) {
2657
3458
  log.blank();
@@ -2749,14 +3550,15 @@ function cmdClear(args, config) {
2749
3550
  return 0;
2750
3551
  }
2751
3552
  function printReport(built) {
2752
- const d = built.payload.data;
3553
+ const s = built.summary ?? {};
2753
3554
  log.blank();
2754
3555
  box("Payload", [
2755
- `type ${built.payload.type}`,
2756
- `store ${d.store_url}`,
2757
- `customer ${d.customer.shipping_address.customer_name} \xB7 ${d.phone}`,
2758
- `items ${d.line_items.map((i) => `${i.name} x${i.quantity}`).join(", ")}`,
2759
- `total ${d.total_price} ${d.line_items[0]?.currency} (was ${d.original_total_price}, discount ${d.total_discount})`,
3556
+ `provider ${built.provider.label}`,
3557
+ `event ${s.event ?? "\u2014"}`,
3558
+ `store ${s.store ?? "\u2014"}`,
3559
+ `customer ${s.customer ?? "\u2014"}`,
3560
+ `items ${s.items ?? "\u2014"}`,
3561
+ `total ${s.total ?? "\u2014"} ${s.currency ?? ""}${s.extra ? " (" + s.extra + ")" : ""}`,
2760
3562
  `saved ${built.file}`
2761
3563
  ], c.green);
2762
3564
  log.blank();
@@ -2778,7 +3580,7 @@ function printReport(built) {
2778
3580
  }
2779
3581
 
2780
3582
  // src/cli.jsx
2781
- import { jsx as jsx15 } from "react/jsx-runtime";
3583
+ import { jsx as jsx16 } from "react/jsx-runtime";
2782
3584
  var HELP = `
2783
3585
  ${c.bold("hookwright")} \u2014 generate real, signed provider webhooks from a live Shopify catalogue
2784
3586
 
@@ -2796,6 +3598,9 @@ ${c.bold("COMMANDS")}
2796
3598
  help show this message
2797
3599
 
2798
3600
  ${c.bold("BUILD OPTIONS")}
3601
+ --provider <id> cashfree-occ | razorpay-magic | nitro (default cashfree-occ)
3602
+ --event <name> nitro only: view \xB7 category_view \xB7 product_view \xB7 addtocart
3603
+ removefromcart \xB7 checkout \xB7 orders/create \xB7 orders/updated
2799
3604
  --items <n> how many products to put in the cart (default 1)
2800
3605
  --search <text> only consider products matching a title
2801
3606
  --discount <n> cart discount in major currency units (default 0)
@@ -2811,6 +3616,7 @@ ${c.bold("SEND OPTIONS")}
2811
3616
  --file <path> send a specific saved payload
2812
3617
  --dry-run print the request without sending
2813
3618
  --force send even if pre-flight fails or target looks like prod
3619
+ --no-endpoint-check skip verifying the destination exists before sending
2814
3620
 
2815
3621
  ${c.bold("CLEAR OPTIONS")}
2816
3622
  --payloads delete saved payloads
@@ -2864,7 +3670,7 @@ async function main() {
2864
3670
  const config = loadConfig(args.flags.config);
2865
3671
  if (command && command !== "ui") {
2866
3672
  if (command === "configure" && process.stdin.isTTY) {
2867
- render(/* @__PURE__ */ jsx15(App, { config, initialScreen: "connect" }));
3673
+ render(/* @__PURE__ */ jsx16(App, { config, initialScreen: "connect" }));
2868
3674
  return;
2869
3675
  }
2870
3676
  const code = await headless(command, args, config);
@@ -2877,11 +3683,14 @@ async function main() {
2877
3683
  process.exitCode = 1;
2878
3684
  return;
2879
3685
  }
2880
- render(/* @__PURE__ */ jsx15(App, { config, initialScreen: "home" }));
3686
+ render(/* @__PURE__ */ jsx16(App, { config, initialScreen: "home" }));
2881
3687
  }
2882
3688
  main().catch((err) => {
2883
3689
  console.error(c.red(`
2884
3690
  ${err.message}`));
3691
+ for (const failure of err.failures ?? []) {
3692
+ console.error(` ${c.red(sym.cross)} ${c.bold(failure.name)}: ${failure.detail}`);
3693
+ }
2885
3694
  if (err.hint) console.error(c.yellow(` ${err.hint}`));
2886
3695
  process.exitCode = 1;
2887
3696
  });