hookwright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/dist/cli.js +2890 -0
  4. package/package.json +44 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2890 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // package.json
18
+ var package_exports = {};
19
+ __export(package_exports, {
20
+ default: () => package_default
21
+ });
22
+ var package_default;
23
+ var init_package = __esm({
24
+ "package.json"() {
25
+ package_default = {
26
+ name: "hookwright",
27
+ version: "1.0.0",
28
+ description: "Build real, signed e-commerce webhooks from a live Shopify catalogue \u2014 interactive terminal UI, no backend",
29
+ keywords: [
30
+ "webhook",
31
+ "shopify",
32
+ "cashfree",
33
+ "testing",
34
+ "hmac",
35
+ "cli",
36
+ "ink",
37
+ "abandoned-cart",
38
+ "payload-generator"
39
+ ],
40
+ license: "MIT",
41
+ type: "module",
42
+ bin: {
43
+ hookwright: "dist/cli.js"
44
+ },
45
+ files: [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ scripts: {
51
+ build: "node scripts/build.mjs",
52
+ dev: "node scripts/build.mjs --watch",
53
+ start: "npm run build --silent && node dist/cli.js",
54
+ test: "node --test test/*.test.mjs",
55
+ prepublishOnly: "npm run build && npm test"
56
+ },
57
+ engines: {
58
+ node: ">=20"
59
+ },
60
+ dependencies: {
61
+ "@inkjs/ui": "^2.0.0",
62
+ ink: "^7.1.1",
63
+ react: "^19.2.8"
64
+ },
65
+ devDependencies: {
66
+ esbuild: "^0.28.2"
67
+ }
68
+ };
69
+ }
70
+ });
71
+
72
+ // src/cli.jsx
73
+ import React15 from "react";
74
+ import { render } from "ink";
75
+
76
+ // src/ui/App.jsx
77
+ import React14, { useState as useState8 } from "react";
78
+ import { useApp as useApp2 } from "ink";
79
+
80
+ // src/ui/screens/Home.jsx
81
+ import React2 from "react";
82
+ import { Box as Box2, Text as Text2 } from "ink";
83
+ import { Select } from "@inkjs/ui";
84
+
85
+ // src/ui/components/Header.jsx
86
+ import React from "react";
87
+ import { Box, Text } from "ink";
88
+
89
+ // src/ui/theme.js
90
+ var palette = {
91
+ accent: "cyan",
92
+ ok: "green",
93
+ warn: "yellow",
94
+ bad: "red",
95
+ dim: "gray",
96
+ heading: "magenta"
97
+ };
98
+ var glyph = {
99
+ tick: "\u2714",
100
+ cross: "\u2716",
101
+ warn: "\u25B2",
102
+ dot: "\u2022",
103
+ arrow: "\u203A",
104
+ pointer: "\u276F"
105
+ };
106
+ function mask(value) {
107
+ const s = String(value ?? "");
108
+ if (!s) return "";
109
+ if (s.length <= 8) return "*".repeat(s.length);
110
+ return s.slice(0, 4) + "*".repeat(Math.max(4, s.length - 8)) + s.slice(-4);
111
+ }
112
+ function truncate(value, max = 52) {
113
+ const s = String(value ?? "");
114
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
115
+ }
116
+ function money(value, currency) {
117
+ if (value == null) return "\u2014";
118
+ return `${Number(value).toFixed(2)} ${currency ?? ""}`.trim();
119
+ }
120
+
121
+ // src/ui/components/Header.jsx
122
+ import { jsx, jsxs } from "react/jsx-runtime";
123
+ function Header({ title, subtitle, right }) {
124
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
125
+ /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: palette.accent, paddingX: 1, justifyContent: "space-between", children: [
126
+ /* @__PURE__ */ jsx(Text, { bold: true, color: palette.accent, children: title }),
127
+ right ? /* @__PURE__ */ jsx(Text, { color: palette.dim, children: truncate(right, 44) }) : null
128
+ ] }),
129
+ subtitle ? /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: palette.dim, children: subtitle }) }) : null
130
+ ] });
131
+ }
132
+ function Section({ title, children }) {
133
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
134
+ /* @__PURE__ */ jsx(Text, { bold: true, color: palette.heading, children: title }),
135
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 0, children })
136
+ ] });
137
+ }
138
+
139
+ // src/config.mjs
140
+ import fs from "node:fs";
141
+ import os from "node:os";
142
+ import path from "node:path";
143
+ var HOME = process.env.HOOKWRIGHT_HOME || process.env.HOOKFORGE_HOME || process.env.WEBHOOK_FORGE_HOME;
144
+ var xdgConfig = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
145
+ var xdgState = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
146
+ var LEGACY_NAMES = ["hookforge", "webhook-forge"];
147
+ function pick(base, name) {
148
+ const preferred = path.join(base, name);
149
+ if (fs.existsSync(preferred)) return preferred;
150
+ for (const legacy of LEGACY_NAMES) {
151
+ const candidate = path.join(base, legacy);
152
+ if (fs.existsSync(candidate)) return candidate;
153
+ }
154
+ return preferred;
155
+ }
156
+ var CONFIG_HOME = HOME || pick(xdgConfig, "hookwright");
157
+ var STATE_HOME = HOME || pick(xdgState, "hookwright");
158
+ var CONFIG_PATH = process.env.HOOKWRIGHT_CONFIG || process.env.HOOKFORGE_CONFIG || process.env.WEBHOOK_FORGE_CONFIG || path.join(CONFIG_HOME, "config.json");
159
+ var STATE_DIR = STATE_HOME;
160
+ var OUT_DIR = path.join(STATE_HOME, "payloads");
161
+ var DEFAULT_CONFIG = {
162
+ shopify: {
163
+ domain: "",
164
+ accessToken: "",
165
+ apiVersion: "2024-10",
166
+ // cached from the last successful connection, for display only
167
+ shop: null
168
+ },
169
+ // One destination per provider — set globally in Setup, reused on every build.
170
+ targets: {
171
+ "cashfree-occ": { webhookUrl: "", webhookSecret: "" },
172
+ "razorpay-magic": { webhookUrl: "", webhookSecret: "" },
173
+ nitro: { webhookUrl: "", webhookSecret: "" }
174
+ },
175
+ customer: {
176
+ firstName: "",
177
+ lastName: "",
178
+ email: "",
179
+ phone: "",
180
+ address1: "",
181
+ address2: "",
182
+ city: "",
183
+ province: "",
184
+ provinceCode: "",
185
+ country: "",
186
+ countryCode: "IN",
187
+ zip: ""
188
+ },
189
+ allowedPhones: [],
190
+ defaults: {
191
+ platform: "shopify",
192
+ currency: "INR",
193
+ copyToClipboard: true,
194
+ utm: {
195
+ utm_source: "facebook",
196
+ utm_medium: "cpc",
197
+ utm_campaign: "abandoned-cart-test",
198
+ utm_content: "",
199
+ fbclid: ""
200
+ }
201
+ }
202
+ };
203
+ function deepMerge(base, extra) {
204
+ const out = Array.isArray(base) ? [...base] : { ...base };
205
+ for (const [k, v] of Object.entries(extra ?? {})) {
206
+ if (v && typeof v === "object" && !Array.isArray(v) && base?.[k] && typeof base[k] === "object" && !Array.isArray(base[k])) {
207
+ out[k] = deepMerge(base[k], v);
208
+ } else if (v !== void 0) {
209
+ out[k] = v;
210
+ }
211
+ }
212
+ return out;
213
+ }
214
+ function resolveConfigPath(explicit) {
215
+ return explicit ? path.resolve(String(explicit)) : CONFIG_PATH;
216
+ }
217
+ function migrate(cfg) {
218
+ const legacy = cfg.target;
219
+ if (legacy && (legacy.webhookUrl || legacy.webhookSecret)) {
220
+ const existing = cfg.targets["cashfree-occ"];
221
+ if (!existing.webhookUrl && !existing.webhookSecret) {
222
+ cfg.targets["cashfree-occ"] = {
223
+ webhookUrl: legacy.webhookUrl ?? "",
224
+ webhookSecret: legacy.webhookSecret ?? ""
225
+ };
226
+ }
227
+ }
228
+ delete cfg.target;
229
+ return cfg;
230
+ }
231
+ function loadConfig(explicitPath) {
232
+ const file = resolveConfigPath(explicitPath);
233
+ if (!fs.existsSync(file)) return { ...structuredClone(DEFAULT_CONFIG), _new: true, _path: file };
234
+ try {
235
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
236
+ return { ...migrate(deepMerge(DEFAULT_CONFIG, raw)), _path: file };
237
+ } catch (err) {
238
+ throw new Error(`${file} is not valid JSON: ${err.message}`);
239
+ }
240
+ }
241
+ function targetFor(cfg, providerId) {
242
+ return cfg.targets?.[providerId] ?? { webhookUrl: "", webhookSecret: "" };
243
+ }
244
+ function setTarget(cfg, providerId, patch) {
245
+ cfg.targets = cfg.targets ?? {};
246
+ cfg.targets[providerId] = { ...targetFor(cfg, providerId), ...patch };
247
+ return cfg;
248
+ }
249
+ function configuredTargets(cfg) {
250
+ return Object.entries(cfg.targets ?? {}).filter(([, t]) => t.webhookUrl);
251
+ }
252
+ function saveConfig(cfg, explicitPath) {
253
+ const file = resolveConfigPath(explicitPath ?? cfg._path);
254
+ const clean = { ...cfg };
255
+ delete clean._new;
256
+ delete clean._path;
257
+ fs.mkdirSync(path.dirname(file), { recursive: true });
258
+ fs.writeFileSync(file, JSON.stringify(clean, null, 2) + "\n", { mode: 384 });
259
+ try {
260
+ fs.chmodSync(file, 384);
261
+ } catch {
262
+ }
263
+ return file;
264
+ }
265
+ function applyShopDefaults(cfg, shop, productCount) {
266
+ const applied = [];
267
+ if (!shop) return applied;
268
+ cfg.shopify.shop = {
269
+ name: shop.name,
270
+ domain: shop.domain,
271
+ myshopifyDomain: shop.myshopify_domain,
272
+ currency: shop.currency,
273
+ countryCode: shop.country_code,
274
+ countryName: shop.country_name,
275
+ timezone: shop.iana_timezone,
276
+ plan: shop.plan_display_name,
277
+ email: shop.email,
278
+ productCount: productCount ?? null,
279
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
280
+ };
281
+ if (shop.currency && cfg.defaults.currency !== shop.currency) {
282
+ cfg.defaults.currency = shop.currency;
283
+ applied.push(`currency set to ${shop.currency}`);
284
+ }
285
+ const fill = (path6, value, label) => {
286
+ if (!value) return;
287
+ const keys = path6.split(".");
288
+ const last = keys.pop();
289
+ const target = keys.reduce((a, k) => a[k], cfg);
290
+ if (!target[last]) {
291
+ target[last] = value;
292
+ applied.push(label);
293
+ }
294
+ };
295
+ fill("customer.countryCode", shop.country_code, `country code set to ${shop.country_code}`);
296
+ fill("customer.country", shop.country_name, `country set to ${shop.country_name}`);
297
+ fill("customer.province", shop.province, `province set to ${shop.province}`);
298
+ fill("customer.provinceCode", shop.province_code, `province code set to ${shop.province_code}`);
299
+ fill("customer.city", shop.city, `city set to ${shop.city}`);
300
+ fill("customer.zip", shop.zip, `zip set to ${shop.zip}`);
301
+ fill("customer.email", shop.email, `contact email set to ${shop.email}`);
302
+ fill("customer.phone", shop.phone, `test phone set to the store contact number`);
303
+ return applied;
304
+ }
305
+ function isShopifyConnected(cfg) {
306
+ return Boolean(cfg.shopify.domain && cfg.shopify.accessToken);
307
+ }
308
+ function canSend(cfg, providerId = "cashfree-occ") {
309
+ const target = targetFor(cfg, providerId);
310
+ return Boolean(target.webhookUrl && cfg.customer.phone);
311
+ }
312
+ function configStatus(cfg, providerId = "cashfree-occ") {
313
+ const target = targetFor(cfg, providerId);
314
+ return [
315
+ { key: "Shopify domain", value: cfg.shopify.domain, ok: Boolean(cfg.shopify.domain) },
316
+ { key: "Shopify token", value: cfg.shopify.accessToken, ok: Boolean(cfg.shopify.accessToken), secret: true },
317
+ { key: "Webhook URL", value: target.webhookUrl, ok: Boolean(target.webhookUrl) },
318
+ { key: "Webhook secret", value: target.webhookSecret, ok: Boolean(target.webhookSecret), secret: true },
319
+ { key: "Test phone", value: cfg.customer.phone, ok: Boolean(cfg.customer.phone) },
320
+ { key: "Ship-to country", value: cfg.customer.countryCode, ok: Boolean(cfg.customer.countryCode) }
321
+ ];
322
+ }
323
+ function isConfigured(cfg, providerId = "cashfree-occ") {
324
+ return configStatus(cfg, providerId).every((r) => r.ok);
325
+ }
326
+
327
+ // src/ui/screens/Home.jsx
328
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
329
+ function Home({ config, onPick, onQuit }) {
330
+ const ready = canSend(config);
331
+ const shop = config.shopify.shop;
332
+ const options = [
333
+ { label: "Setup".padEnd(18) + (ready ? "Shopify and webhook destination" : "connect Shopify and set the webhook URL"), value: "setup" },
334
+ { label: "Integrations".padEnd(18) + "build and send a provider webhook", value: "integrations" },
335
+ { label: "History".padEnd(18) + "review or re-send a saved payload", value: "history" },
336
+ { label: "Doctor".padEnd(18) + "check credentials and connectivity", value: "doctor" },
337
+ { label: "Clear".padEnd(18) + "remove cached data", value: "clear" },
338
+ { label: "Quit".padEnd(18), value: "quit" }
339
+ ];
340
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
341
+ /* @__PURE__ */ jsx2(
342
+ Header,
343
+ {
344
+ title: "hookwright",
345
+ subtitle: "real catalogue \u2192 signed provider webhooks",
346
+ right: shop?.name ?? config.shopify.domain ?? "not connected"
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx2(
350
+ Select,
351
+ {
352
+ visibleOptionCount: options.length,
353
+ options,
354
+ onChange: (value) => value === "quit" ? onQuit() : onPick(value)
355
+ }
356
+ ),
357
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "\u2191\u2193 move \xB7 enter select \xB7 q quit" }) })
358
+ ] });
359
+ }
360
+
361
+ // src/ui/screens/Setup.jsx
362
+ import React3 from "react";
363
+ import { Box as Box3, Text as Text3, useInput } from "ink";
364
+ import { Select as Select2 } from "@inkjs/ui";
365
+
366
+ // src/providers/cashfree-occ.mjs
367
+ import crypto from "node:crypto";
368
+
369
+ // src/shopify/products.mjs
370
+ function normalizeProduct(product) {
371
+ const variants = (product.variants ?? []).map((v) => ({
372
+ id: String(v.id),
373
+ title: v.title,
374
+ sku: v.sku || "",
375
+ price: v.price != null ? Number(v.price) : null,
376
+ available: v.inventory_quantity == null ? true : v.inventory_quantity > 0,
377
+ imageId: v.image_id ? String(v.image_id) : null
378
+ }));
379
+ const images = (product.images ?? []).map((img) => ({ id: String(img.id), src: img.src }));
380
+ const defaultImage = product.image?.src ?? images[0]?.src ?? "";
381
+ return {
382
+ id: String(product.id),
383
+ title: product.title,
384
+ handle: product.handle,
385
+ status: product.status,
386
+ vendor: product.vendor,
387
+ productType: product.product_type,
388
+ images,
389
+ image: defaultImage,
390
+ variants
391
+ };
392
+ }
393
+ function imageForVariant(product, variant) {
394
+ if (variant?.imageId) {
395
+ const hit = product.images.find((i) => i.id === variant.imageId);
396
+ if (hit) return hit.src;
397
+ }
398
+ return product.image;
399
+ }
400
+ function cartPermalink(domain, items) {
401
+ const parts = items.filter((i) => i.variantId).map((i) => `${i.variantId}:${i.quantity}`).join(",");
402
+ const clean = String(domain).replace(/^https?:\/\//i, "");
403
+ const scheme = /^(127\.0\.0\.1|localhost)(:\d+)?$/i.test(clean) ? "http" : "https";
404
+ return `${scheme}://${clean}/cart/${parts}`;
405
+ }
406
+ function money2(value) {
407
+ return Math.round(Number(value) * 100) / 100;
408
+ }
409
+
410
+ // src/providers/cashfree-occ.mjs
411
+ var GATE_PATH = "type";
412
+ var GATE_VALUE = "ABANDONED_CHECKOUT";
413
+ var fieldMap = [
414
+ { source: "type", target: "\xABgate\xBB", required: true, note: `must equal ${GATE_VALUE}` },
415
+ { source: "event_time", target: "eventTime" },
416
+ { source: "data.cart_id", target: "cartId" },
417
+ { source: "data.cart_token", target: "cartToken" },
418
+ { source: "data.store_url", target: "storeUrl", required: true },
419
+ { source: "data.platform", target: "platform" },
420
+ { source: "data.email", target: "email" },
421
+ { source: "data.phone", target: "phone", required: true, note: "or shipping_address.phone; must parse to E.164" },
422
+ { source: "data.abandoned_checkout_url", target: "abandonedCheckoutUrl / checkoutUrl", required: true },
423
+ { source: "data.original_total_price", target: "pricing.originalTotalPrice" },
424
+ { source: "data.total_price", target: "pricing.totalPrice", required: true },
425
+ { source: "data.total_discount", target: "pricing.totalDiscount" },
426
+ { source: "data.customer.first_name", target: "customer.firstName" },
427
+ { source: "data.customer.last_name", target: "customer.lastName" },
428
+ { source: "data.customer.email", target: "customer.email" },
429
+ { source: "data.customer.shipping_address.customer_name", target: "customer.customerName" },
430
+ { source: "data.customer.shipping_address.phone", target: "phone fallback" },
431
+ { source: "data.customer.shipping_address.address1", target: "customer.address1" },
432
+ { source: "data.customer.shipping_address.address2", target: "customer.address2" },
433
+ { source: "data.customer.shipping_address.city", target: "customer.city" },
434
+ { source: "data.customer.shipping_address.province", target: "customer.province" },
435
+ { source: "data.customer.shipping_address.province_code", target: "customer.provinceCode" },
436
+ { source: "data.customer.shipping_address.country", target: "customer.country" },
437
+ { source: "data.customer.shipping_address.country_code", target: "customer.countryCode", required: true, note: "region code used to parse the phone" },
438
+ { source: "data.customer.shipping_address.zip", target: "customer.zip" },
439
+ { source: "data.line_items[0].name", target: "cart.productName", required: true },
440
+ { source: "data.line_items[0].currency", target: "pricing.currency", required: true, note: "currency is read off the FIRST line item, not the root" },
441
+ { source: "data.line_items[0].image_url", target: "cart.image", required: true, note: "first line item with an image wins" },
442
+ { source: "data.line_items[0].quantity", target: "cart.quantity" },
443
+ { source: "data.utm_parameters.utm_source", target: "utm.source" },
444
+ { source: "data.utm_parameters.utm_medium", target: "utm.medium" },
445
+ { source: "data.utm_parameters.utm_campaign", target: "utm.campaign" },
446
+ { source: "data.utm_parameters.utm_content", target: "utm.content" },
447
+ { source: "data.utm_parameters.fbclid", target: "utm.fbclid" }
448
+ ];
449
+ function buildPayload(ctx) {
450
+ const { config, items, phone } = ctx;
451
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
452
+ const currency = ctx.currency || config.defaults.currency;
453
+ const domain = config.shopify.domain;
454
+ const cust = config.customer;
455
+ const lineItems = items.map(({ product, variant, quantity }) => ({
456
+ name: product.title,
457
+ variant_id: variant ? Number(variant.id) : null,
458
+ variant_title: variant?.title ?? null,
459
+ reference: Number(product.id),
460
+ sku: variant?.sku || null,
461
+ quantity,
462
+ price: money2(variant?.price ?? 0),
463
+ currency,
464
+ image_url: imageForVariant(product, variant)
465
+ }));
466
+ const originalTotal = money2(lineItems.reduce((sum, li) => sum + li.price * li.quantity, 0));
467
+ const discount = money2(Math.min(ctx.discount ?? 0, originalTotal));
468
+ const total = money2(originalTotal - discount);
469
+ const checkoutUrl = cartPermalink(domain, items.map(({ variant, quantity }) => ({ variantId: variant?.id, quantity })));
470
+ return {
471
+ type: GATE_VALUE,
472
+ event_time: now.toISOString(),
473
+ data: {
474
+ cart_id: crypto.randomUUID(),
475
+ cart_token: crypto.randomBytes(16).toString("hex"),
476
+ store_url: domain,
477
+ platform: config.defaults.platform,
478
+ email: cust.email,
479
+ phone,
480
+ abandoned_checkout_url: checkoutUrl,
481
+ original_total_price: originalTotal,
482
+ total_price: total,
483
+ total_discount: discount,
484
+ customer: {
485
+ first_name: cust.firstName,
486
+ last_name: cust.lastName,
487
+ email: cust.email,
488
+ shipping_address: {
489
+ customer_name: `${cust.firstName} ${cust.lastName}`.trim(),
490
+ phone,
491
+ address1: cust.address1,
492
+ address2: cust.address2,
493
+ city: cust.city,
494
+ province: cust.province,
495
+ province_code: cust.provinceCode,
496
+ country: cust.country,
497
+ country_code: cust.countryCode,
498
+ zip: cust.zip
499
+ }
500
+ },
501
+ line_items: lineItems,
502
+ utm_parameters: { ...config.defaults.utm }
503
+ }
504
+ };
505
+ }
506
+ function sign({ body, secret, timestamp }) {
507
+ const ts = timestamp ?? String(Math.floor(Date.now() / 1e3));
508
+ const signature = crypto.createHmac("sha256", String(secret)).update(`${ts}${body}`).digest("base64");
509
+ return {
510
+ timestamp: ts,
511
+ signature,
512
+ headers: {
513
+ "content-type": "application/json",
514
+ "x-webhook-signature": signature,
515
+ "x-webhook-timestamp": ts
516
+ }
517
+ };
518
+ }
519
+ function webhookUrl(config) {
520
+ return String(targetFor(config, "cashfree-occ").webhookUrl ?? "").trim();
521
+ }
522
+ function webhookSecret(config) {
523
+ return String(targetFor(config, "cashfree-occ").webhookSecret ?? "");
524
+ }
525
+ var editableFields = [
526
+ { group: "Event", path: "event_time", label: "Event time", hint: "ISO timestamp of the abandonment" },
527
+ { group: "Cart", path: "data.cart_id", label: "Cart id" },
528
+ { group: "Cart", path: "data.cart_token", label: "Cart token" },
529
+ { group: "Cart", path: "data.store_url", label: "Store URL" },
530
+ { group: "Cart", path: "data.platform", label: "Platform" },
531
+ { group: "Cart", path: "data.abandoned_checkout_url", label: "Abandoned checkout URL", hint: "the link the shopper is sent back to" },
532
+ { group: "Pricing", path: "data.original_total_price", label: "Original total", type: "number" },
533
+ { group: "Pricing", path: "data.total_discount", label: "Discount", type: "number" },
534
+ { group: "Pricing", path: "data.total_price", label: "Total price", type: "number" },
535
+ { group: "Customer", path: "data.customer.first_name", label: "First name" },
536
+ { group: "Customer", path: "data.customer.last_name", label: "Last name" },
537
+ { group: "Customer", path: "data.email", label: "Email" },
538
+ { group: "Customer", path: "data.phone", label: "Phone", hint: "must parse to E.164 against the country code below" },
539
+ { group: "Shipping", path: "data.customer.shipping_address.customer_name", label: "Ship-to name" },
540
+ { group: "Shipping", path: "data.customer.shipping_address.address1", label: "Address line 1" },
541
+ { group: "Shipping", path: "data.customer.shipping_address.address2", label: "Address line 2", optional: true },
542
+ { group: "Shipping", path: "data.customer.shipping_address.city", label: "City" },
543
+ { group: "Shipping", path: "data.customer.shipping_address.province", label: "Province / state" },
544
+ { group: "Shipping", path: "data.customer.shipping_address.province_code", label: "Province code", optional: true },
545
+ { group: "Shipping", path: "data.customer.shipping_address.country", label: "Country" },
546
+ { group: "Shipping", path: "data.customer.shipping_address.country_code", label: "Country code", hint: "region used to parse the phone" },
547
+ { group: "Shipping", path: "data.customer.shipping_address.zip", label: "Zip / postcode" },
548
+ { group: "UTM", path: "data.utm_parameters.utm_source", label: "UTM source", optional: true },
549
+ { group: "UTM", path: "data.utm_parameters.utm_medium", label: "UTM medium", optional: true },
550
+ { group: "UTM", path: "data.utm_parameters.utm_campaign", label: "UTM campaign", optional: true },
551
+ { group: "UTM", path: "data.utm_parameters.utm_content", label: "UTM content", optional: true },
552
+ { group: "UTM", path: "data.utm_parameters.fbclid", label: "Facebook click id", optional: true }
553
+ ];
554
+ var cashfree_occ_default = {
555
+ id: "cashfree-occ",
556
+ urlKey: "cashfree_occ",
557
+ label: "Cashfree One Click Checkout",
558
+ eventType: "cashfree_occ_abandoned_checkout",
559
+ gate: { path: GATE_PATH, value: GATE_VALUE },
560
+ signatureScheme: "base64 HMAC-SHA256 over `timestamp + body`",
561
+ signatureHeader: "x-webhook-signature",
562
+ fieldMap,
563
+ editableFields,
564
+ buildPayload,
565
+ sign,
566
+ webhookUrl,
567
+ webhookSecret
568
+ };
569
+
570
+ // src/providers/index.mjs
571
+ var PROVIDER_META = [
572
+ {
573
+ id: "cashfree-occ",
574
+ label: "Cashfree One Click Checkout",
575
+ signature: "base64 HMAC-SHA256 over `timestamp + body`",
576
+ header: "x-webhook-signature",
577
+ available: true
578
+ },
579
+ {
580
+ id: "razorpay-magic",
581
+ label: "Razorpay Magic",
582
+ signature: "hex HMAC-SHA256 over the body",
583
+ header: "x-razorpay-signature",
584
+ available: false
585
+ },
586
+ {
587
+ id: "nitro",
588
+ label: "Nitro",
589
+ signature: "static bearer token",
590
+ header: "authorization",
591
+ available: false
592
+ }
593
+ ];
594
+ var providers = {
595
+ [cashfree_occ_default.id]: cashfree_occ_default
596
+ };
597
+ function getProvider(id) {
598
+ const provider = providers[id];
599
+ if (!provider) {
600
+ throw new Error(`unknown provider "${id}" \u2014 available: ${Object.keys(providers).join(", ")}`);
601
+ }
602
+ return provider;
603
+ }
604
+ function listProviders() {
605
+ return Object.values(providers);
606
+ }
607
+ function listProviderMeta() {
608
+ return PROVIDER_META;
609
+ }
610
+ function metaFor(id) {
611
+ return PROVIDER_META.find((p) => p.id === id) ?? { id, label: id, available: Boolean(providers[id]) };
612
+ }
613
+
614
+ // src/ui/screens/Setup.jsx
615
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
616
+ function Setup({ config, onPick, onBack }) {
617
+ const shopifyOk = isShopifyConnected(config);
618
+ const configured = configuredTargets(config).length;
619
+ const total = listProviderMeta().length;
620
+ const phoneOk = Boolean(config.customer.phone);
621
+ const shop = config.shopify.shop;
622
+ useInput((input, key) => {
623
+ if (key.escape) onBack();
624
+ });
625
+ const options = [
626
+ { label: "Shopify".padEnd(20) + (shopifyOk ? `connected \xB7 ${shop?.name ?? config.shopify.domain}` : "connect a store \u2014 domain + Admin API token"), value: "connect" },
627
+ { label: "Webhook".padEnd(20) + `destinations \xB7 ${configured} of ${total} set`, value: "webhook" },
628
+ { label: "Customer".padEnd(20) + (phoneOk ? `${config.customer.firstName} ${config.customer.lastName} \xB7 ${config.customer.phone}` : "shopper name, phone, shipping address"), value: "customer" },
629
+ { label: "Defaults".padEnd(20) + `currency ${config.defaults.currency} \xB7 platform ${config.defaults.platform}`, value: "defaults" },
630
+ { label: "Back".padEnd(20), value: "back" }
631
+ ];
632
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
633
+ /* @__PURE__ */ jsx3(Header, { title: "Setup", subtitle: "the product source and the webhook destination", right: "esc to go back" }),
634
+ /* @__PURE__ */ jsx3(Select2, { visibleOptionCount: options.length, options, onChange: (v) => v === "back" ? onBack() : onPick(v) }),
635
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: palette.dim, children: `${glyph.dot} shared by every integration \u2014 set it once` }) })
636
+ ] });
637
+ }
638
+
639
+ // src/ui/screens/Integrations.jsx
640
+ import React4 from "react";
641
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
642
+ import { Select as Select3 } from "@inkjs/ui";
643
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
644
+ var PLANNED = listProviderMeta().filter((p) => !p.available);
645
+ function Integrations({ config, onPick, onBack, onSetup }) {
646
+ const available = listProviders();
647
+ const ready = available.every((p) => canSend(config, p.id));
648
+ useInput2((input, key) => {
649
+ if (key.escape) onBack();
650
+ });
651
+ const options = [
652
+ ...available.map((p) => ({
653
+ label: p.label.padEnd(34) + (canSend(config, p.id) ? "ready" : "setup incomplete"),
654
+ value: p.id
655
+ })),
656
+ ...ready ? [] : [{ label: "Go to Setup".padEnd(34) + "connect Shopify and set the webhook URL", value: "__setup" }],
657
+ { label: "Back".padEnd(34), value: "__back" }
658
+ ];
659
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
660
+ /* @__PURE__ */ jsx4(Header, { title: "Integrations", subtitle: "pick the provider whose webhook you want to generate", right: "esc to go back" }),
661
+ /* @__PURE__ */ jsx4(
662
+ Select3,
663
+ {
664
+ visibleOptionCount: options.length,
665
+ options,
666
+ onChange: (v) => v === "__back" ? onBack() : v === "__setup" ? onSetup() : onPick(v)
667
+ }
668
+ ),
669
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: PLANNED.map((p) => /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: ` ${glyph.dot} ${p.label.padEnd(32)} not yet available` }, p.id)) })
670
+ ] });
671
+ }
672
+
673
+ // 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";
676
+ import { Spinner, Select as Select4, MultiSelect, TextInput, ConfirmInput, Alert, Badge } from "@inkjs/ui";
677
+
678
+ // src/ui/components/CheckList.jsx
679
+ import React5 from "react";
680
+ import { Box as Box5, Text as Text5 } from "ink";
681
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
682
+ function CheckList({ checks }) {
683
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: checks.map((check, i) => {
684
+ const colour = check.ok ? palette.ok : check.warn ? palette.warn : palette.bad;
685
+ const mark = check.ok ? glyph.tick : check.warn ? glyph.warn : glyph.cross;
686
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
687
+ /* @__PURE__ */ jsx5(Text5, { color: colour, children: ` ${mark} ` }),
688
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: check.name.padEnd(24) }),
689
+ /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: check.detail })
690
+ ] }, i);
691
+ }) });
692
+ }
693
+ function KeyValue({ rows, keyWidth = 14 }) {
694
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: rows.map(([k, v, colour], i) => /* @__PURE__ */ jsxs5(Box5, { children: [
695
+ /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: ` ${String(k).padEnd(keyWidth)}` }),
696
+ /* @__PURE__ */ jsx5(Text5, { color: colour, children: String(v) })
697
+ ] }, i)) });
698
+ }
699
+
700
+ // src/ui/components/FieldTable.jsx
701
+ import React6 from "react";
702
+ import { Box as Box6, Text as Text6 } from "ink";
703
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
704
+ function FieldTable({ coverage: coverage2, limit }) {
705
+ const rows = limit ? coverage2.slice(0, limit) : coverage2;
706
+ const fieldWidth = Math.min(46, Math.max(...rows.map((r) => r.field.length)) + 1);
707
+ const mapWidth = Math.min(30, Math.max(...rows.map((r) => String(r.maps_to).length)) + 1);
708
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
709
+ /* @__PURE__ */ jsxs6(Box6, { children: [
710
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: palette.dim, children: " " + "payload field".padEnd(fieldWidth) }),
711
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: palette.dim, children: "maps to".padEnd(mapWidth) }),
712
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: palette.dim, children: "value" })
713
+ ] }),
714
+ rows.map((row, i) => {
715
+ const colour = row.status === "MISSING" ? palette.bad : row.status === "empty" ? palette.dim : void 0;
716
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
717
+ /* @__PURE__ */ jsx6(Text6, { color: colour, children: " " + truncate(row.field, fieldWidth - 1).padEnd(fieldWidth) }),
718
+ /* @__PURE__ */ jsx6(Text6, { color: palette.dim, children: truncate(String(row.maps_to), mapWidth - 1).padEnd(mapWidth) }),
719
+ /* @__PURE__ */ jsx6(Text6, { color: colour, children: truncate(row.value, 34) })
720
+ ] }, i);
721
+ }),
722
+ limit && coverage2.length > limit ? /* @__PURE__ */ jsx6(Text6, { color: palette.dim, children: ` \u2026 ${coverage2.length - limit} more fields` }) : null
723
+ ] });
724
+ }
725
+
726
+ // src/errors.mjs
727
+ var ForgeError = class extends Error {
728
+ constructor(message, { hint, cause } = {}) {
729
+ super(message);
730
+ this.name = "ForgeError";
731
+ this.hint = hint;
732
+ if (cause) this.cause = cause;
733
+ }
734
+ };
735
+ var ConfigError = class extends ForgeError {
736
+ constructor(message, hint) {
737
+ super(message, { hint });
738
+ this.name = "ConfigError";
739
+ }
740
+ };
741
+ var ShopifyError = class extends ForgeError {
742
+ constructor(message, { status, body, hint } = {}) {
743
+ super(message, { hint });
744
+ this.name = "ShopifyError";
745
+ this.status = status;
746
+ this.body = body;
747
+ }
748
+ };
749
+
750
+ // src/logger.mjs
751
+ import fs2 from "node:fs";
752
+ import path2 from "node:path";
753
+ var LOG_PATH = path2.join(STATE_DIR, "forge.log");
754
+ var verbose = false;
755
+ function setVerbose(v) {
756
+ verbose = Boolean(v);
757
+ }
758
+ function isVerbose() {
759
+ return verbose;
760
+ }
761
+ function audit(event, detail = {}) {
762
+ try {
763
+ fs2.mkdirSync(STATE_DIR, { recursive: true });
764
+ const line = JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), event, ...redact(detail) });
765
+ fs2.appendFileSync(LOG_PATH, line + "\n");
766
+ } catch {
767
+ }
768
+ }
769
+ function redact(obj) {
770
+ const SECRET_KEYS = /token|secret|password|signature|authorization/i;
771
+ const walk = (v) => {
772
+ if (Array.isArray(v)) return v.map(walk);
773
+ if (v && typeof v === "object") {
774
+ return Object.fromEntries(Object.entries(v).map(([k, val]) => [k, SECRET_KEYS.test(k) ? "\xABredacted\xBB" : walk(val)]));
775
+ }
776
+ return v;
777
+ };
778
+ return walk(obj);
779
+ }
780
+ var logPath = LOG_PATH;
781
+
782
+ // src/shopify/client.mjs
783
+ var DEFAULT_API_VERSION = "2024-10";
784
+ var ShopifyClient = class {
785
+ constructor({ domain, accessToken, apiVersion = DEFAULT_API_VERSION }) {
786
+ if (!domain) throw new ShopifyError("Shopify domain missing", { hint: "run `hookwright configure`" });
787
+ if (!accessToken) throw new ShopifyError("Shopify access token missing", { hint: "run `hookwright configure`" });
788
+ this.domain = normalizeDomain(domain);
789
+ this.protocol = /^http:\/\//i.test(String(domain).trim()) || /^(127\.0\.0\.1|localhost)(:\d+)?$/i.test(this.domain) ? "http" : "https";
790
+ this.accessToken = accessToken;
791
+ this.apiVersion = apiVersion || DEFAULT_API_VERSION;
792
+ }
793
+ get base() {
794
+ return `${this.protocol}://${this.domain}/admin/api/${this.apiVersion}`;
795
+ }
796
+ async request(pathOrUrl, { method = "GET", query, body, timeoutMs = 2e4 } = {}) {
797
+ const url = pathOrUrl.startsWith("http") ? new URL(pathOrUrl) : new URL(this.base + pathOrUrl);
798
+ for (const [k, v] of Object.entries(query ?? {})) {
799
+ if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
800
+ }
801
+ const controller = new AbortController();
802
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
803
+ let res;
804
+ try {
805
+ res = await fetch(url, {
806
+ method,
807
+ signal: controller.signal,
808
+ headers: {
809
+ "X-Shopify-Access-Token": this.accessToken,
810
+ "Content-Type": "application/json",
811
+ Accept: "application/json"
812
+ },
813
+ body: body ? JSON.stringify(body) : void 0
814
+ });
815
+ } catch (err) {
816
+ clearTimeout(timer);
817
+ if (err.name === "AbortError") throw new ShopifyError(`Shopify request timed out after ${timeoutMs}ms`, { hint: "check the store domain and your network" });
818
+ throw new ShopifyError(`Shopify request failed: ${err.message}`, { hint: "check the store domain" });
819
+ }
820
+ clearTimeout(timer);
821
+ const text = await res.text();
822
+ let json = null;
823
+ try {
824
+ json = text ? JSON.parse(text) : null;
825
+ } catch {
826
+ }
827
+ if (isVerbose()) audit("shopify.request", { url: url.pathname, status: res.status });
828
+ if (res.status === 401 || res.status === 403) {
829
+ throw new ShopifyError(`Shopify rejected the credentials (${res.status})`, {
830
+ status: res.status,
831
+ body: json ?? text,
832
+ hint: "the Admin API access token needs read_products scope, and must belong to this store"
833
+ });
834
+ }
835
+ if (res.status === 404) {
836
+ throw new ShopifyError("Shopify returned 404 \u2014 wrong store domain or API version", {
837
+ status: 404,
838
+ hint: `tried ${url.host}${url.pathname}`
839
+ });
840
+ }
841
+ if (res.status === 429) {
842
+ const retry = Number(res.headers.get("retry-after") ?? 2);
843
+ await sleep(retry * 1e3);
844
+ return this.request(pathOrUrl, { method, query, body, timeoutMs });
845
+ }
846
+ if (!res.ok) {
847
+ throw new ShopifyError(`Shopify error ${res.status}`, { status: res.status, body: json ?? text });
848
+ }
849
+ return { json, headers: res.headers };
850
+ }
851
+ /** Verifies the credentials and returns the real shop record. */
852
+ async shop() {
853
+ const { json } = await this.request("/shop.json");
854
+ return json?.shop ?? null;
855
+ }
856
+ /**
857
+ * Products, following Link-header pagination.
858
+ * @param {{limit?: number, title?: string, pages?: number, status?: string}} opts
859
+ */
860
+ async products({ limit = 50, title, pages = 1, status = "active" } = {}) {
861
+ const out = [];
862
+ let next = null;
863
+ for (let page = 0; page < pages; page += 1) {
864
+ const { json, headers } = next ? await this.request(next) : await this.request("/products.json", { query: { limit, title, status } });
865
+ out.push(...json?.products ?? []);
866
+ next = parseNextLink(headers.get("link"));
867
+ if (!next) break;
868
+ }
869
+ return out;
870
+ }
871
+ async productCount() {
872
+ const { json } = await this.request("/products/count.json");
873
+ return json?.count ?? 0;
874
+ }
875
+ };
876
+ function normalizeDomain(domain) {
877
+ return String(domain).trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "").toLowerCase();
878
+ }
879
+ function parseNextLink(linkHeader) {
880
+ if (!linkHeader) return null;
881
+ const match = linkHeader.split(",").find((part) => part.includes('rel="next"'));
882
+ if (!match) return null;
883
+ const url = match.match(/<([^>]+)>/);
884
+ return url ? url[1] : null;
885
+ }
886
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
887
+
888
+ // src/core/catalogue.mjs
889
+ async function verifyStore(cfg) {
890
+ const client = new ShopifyClient(cfg.shopify);
891
+ const shop = await client.shop();
892
+ const count = await client.productCount().catch(() => null);
893
+ return { shop, count };
894
+ }
895
+ async function fetchCatalogue(cfg, { search, pages = 1, limit = 50 } = {}) {
896
+ const client = new ShopifyClient(cfg.shopify);
897
+ const raw = await client.products({ title: search, pages, limit });
898
+ return raw.map(normalizeProduct).filter((p) => p.variants.length);
899
+ }
900
+ function autoSelect(products, count = 1) {
901
+ return products.slice(0, count).map((product) => ({
902
+ product,
903
+ variant: product.variants[0],
904
+ quantity: 1
905
+ }));
906
+ }
907
+
908
+ // src/phone.mjs
909
+ var CALLING_CODES = {
910
+ IN: "91",
911
+ US: "1",
912
+ CA: "1",
913
+ GB: "44",
914
+ AE: "971",
915
+ SG: "65",
916
+ AU: "61",
917
+ NZ: "64",
918
+ DE: "49",
919
+ FR: "33",
920
+ IT: "39",
921
+ ES: "34",
922
+ NL: "31",
923
+ SE: "46",
924
+ RO: "40",
925
+ PL: "48",
926
+ ZA: "27",
927
+ NG: "234",
928
+ KE: "254",
929
+ BD: "880",
930
+ PK: "92",
931
+ LK: "94",
932
+ NP: "977",
933
+ MY: "60",
934
+ ID: "62",
935
+ PH: "63",
936
+ TH: "66",
937
+ VN: "84",
938
+ JP: "81",
939
+ KR: "82",
940
+ CN: "86",
941
+ SA: "966",
942
+ QA: "974",
943
+ KW: "965",
944
+ OM: "968",
945
+ BH: "973",
946
+ BR: "55",
947
+ MX: "52"
948
+ };
949
+ function callingCode(regionCode) {
950
+ return CALLING_CODES[String(regionCode || "").toUpperCase()] || null;
951
+ }
952
+ function toE164(raw, regionCode) {
953
+ const cleaned = String(raw ?? "").replace(/\s+/g, "");
954
+ if (!cleaned) return { ok: false, reason: "phone is empty" };
955
+ if (cleaned.startsWith("+")) {
956
+ const digits2 = cleaned.slice(1).replace(/\D/g, "");
957
+ if (digits2.length < 8 || digits2.length > 15) return { ok: false, reason: `+${digits2} is not a plausible E.164 length` };
958
+ return { ok: true, value: `+${digits2}` };
959
+ }
960
+ const cc = callingCode(regionCode);
961
+ if (!cc) {
962
+ return {
963
+ ok: false,
964
+ reason: `no country calling code known for region "${regionCode}" \u2014 give the phone in +E.164 form instead`
965
+ };
966
+ }
967
+ let national = cleaned.replace(/\D/g, "").replace(/^0+/, "");
968
+ if (national.startsWith(cc) && national.length > 10) national = national.slice(cc.length);
969
+ const value = `+${cc}${national}`;
970
+ const digits = value.slice(1);
971
+ if (digits.length < 8 || digits.length > 15) return { ok: false, reason: `${value} is not a plausible E.164 length` };
972
+ return { ok: true, value };
973
+ }
974
+ var knownRegions = Object.keys(CALLING_CODES);
975
+
976
+ // src/core/paths.mjs
977
+ function getPath(obj, path6) {
978
+ return String(path6).replace(/\[(\d+)\]/g, ".$1").split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
979
+ }
980
+ function setPath(obj, path6, value) {
981
+ const keys = String(path6).replace(/\[(\d+)\]/g, ".$1").split(".");
982
+ const last = keys.pop();
983
+ const target = keys.reduce((acc, key) => {
984
+ if (acc[key] == null || typeof acc[key] !== "object") acc[key] = {};
985
+ return acc[key];
986
+ }, obj);
987
+ target[last] = value;
988
+ return obj;
989
+ }
990
+
991
+ // src/core/validate.mjs
992
+ function checkRoundTrip(body) {
993
+ let reparsed;
994
+ try {
995
+ reparsed = JSON.stringify(JSON.parse(body));
996
+ } catch (err) {
997
+ return { name: "JSON round-trip", ok: false, detail: `body is not valid JSON: ${err.message}` };
998
+ }
999
+ if (reparsed === body) {
1000
+ return { name: "JSON round-trip", ok: true, detail: "signed bytes survive parse \u2192 stringify" };
1001
+ }
1002
+ const at = firstDifference(body, reparsed);
1003
+ return {
1004
+ name: "JSON round-trip",
1005
+ ok: false,
1006
+ detail: `re-serialized body differs at offset ${at.index}: sent ${JSON.stringify(at.a)} vs recomputed ${JSON.stringify(at.b)}`
1007
+ };
1008
+ }
1009
+ function checkGate(payload, provider) {
1010
+ const actual = getPath(payload, provider.gate.path);
1011
+ const ok = actual === provider.gate.value;
1012
+ return {
1013
+ name: "Schema gate",
1014
+ 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`
1016
+ };
1017
+ }
1018
+ 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");
1021
+ if (!phone) {
1022
+ return { name: "Phone", ok: false, detail: "no phone in data.phone or shipping_address.phone \u2014 consumer throws BadPayloadError" };
1023
+ }
1024
+ const parsed = toE164(phone, region);
1025
+ return {
1026
+ name: "Phone",
1027
+ ok: parsed.ok,
1028
+ detail: parsed.ok ? `${phone} \u2192 ${parsed.value} (region ${region || "n/a"})` : parsed.reason
1029
+ };
1030
+ }
1031
+ function checkAllowedPhone(payload, config) {
1032
+ const allowed = config.allowedPhones ?? [];
1033
+ if (!allowed.length) {
1034
+ return { name: "Phone allow-list", ok: true, detail: "no allow-list configured (skipped)", warn: true };
1035
+ }
1036
+ const phone = getPath(payload, "data.phone");
1037
+ const parsed = toE164(phone, getPath(payload, "data.customer.shipping_address.country_code"));
1038
+ const normalized = parsed.ok ? parsed.value : phone;
1039
+ const ok = allowed.some((p) => toE164(p, "IN").value === normalized || p === normalized);
1040
+ return {
1041
+ name: "Phone allow-list",
1042
+ ok,
1043
+ detail: ok ? `${normalized} is an approved test handset` : `${normalized} is NOT in allowedPhones \u2014 refusing to risk messaging a real shopper`
1044
+ };
1045
+ }
1046
+ function coverage(payload, fieldMap2) {
1047
+ return fieldMap2.map((f) => {
1048
+ const value = getPath(payload, f.source);
1049
+ const present = value !== void 0 && value !== null && value !== "";
1050
+ return {
1051
+ field: f.source,
1052
+ maps_to: f.target,
1053
+ value: present ? preview(value) : "\u2014",
1054
+ status: present ? "ok" : f.required ? "MISSING" : "empty",
1055
+ required: Boolean(f.required),
1056
+ note: f.note ?? ""
1057
+ };
1058
+ });
1059
+ }
1060
+ 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))];
1063
+ if (!urls.length) {
1064
+ return [{ name: "Product images", ok: false, detail: "no line item carries an image_url \u2014 cart.image resolves to undefined" }];
1065
+ }
1066
+ const results = await Promise.all(
1067
+ urls.map(async (url) => {
1068
+ const controller = new AbortController();
1069
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1070
+ try {
1071
+ const res = await fetch(url, { method: "HEAD", signal: controller.signal });
1072
+ clearTimeout(timer);
1073
+ return { url, status: res.status, ok: res.ok };
1074
+ } catch (err) {
1075
+ clearTimeout(timer);
1076
+ return { url, status: 0, ok: false, error: err.name === "AbortError" ? "timeout" : err.message };
1077
+ }
1078
+ })
1079
+ );
1080
+ return results.map((r) => ({
1081
+ name: "Product image",
1082
+ ok: r.ok,
1083
+ detail: `${r.ok ? "HTTP " + r.status : "unreachable (" + (r.error ?? r.status) + ")"} \u2014 ${shorten(r.url)}`
1084
+ }));
1085
+ }
1086
+ async function runAll({ payload, body, provider, config, checkImageUrls = true }) {
1087
+ const checks = [checkGate(payload, provider), checkRoundTrip(body), checkPhone(payload), checkAllowedPhone(payload, config)];
1088
+ if (checkImageUrls) checks.push(...await checkImages(payload));
1089
+ const cov = coverage(payload, provider.fieldMap);
1090
+ const missing = cov.filter((c2) => c2.status === "MISSING");
1091
+ checks.push({
1092
+ name: "Required field coverage",
1093
+ ok: missing.length === 0,
1094
+ detail: missing.length === 0 ? `all ${cov.length} mapped fields resolved` : `missing: ${missing.map((m) => m.field).join(", ")}`
1095
+ });
1096
+ return {
1097
+ checks,
1098
+ coverage: cov,
1099
+ ok: checks.filter((c2) => !c2.warn).every((c2) => c2.ok),
1100
+ blocking: checks.filter((c2) => !c2.ok && !c2.warn)
1101
+ };
1102
+ }
1103
+ function preview(value) {
1104
+ if (typeof value === "object") return JSON.stringify(value).slice(0, 48);
1105
+ const s = String(value);
1106
+ return s.length > 48 ? s.slice(0, 45) + "\u2026" : s;
1107
+ }
1108
+ function shorten(url) {
1109
+ const s = String(url);
1110
+ return s.length > 62 ? s.slice(0, 40) + "\u2026" + s.slice(-18) : s;
1111
+ }
1112
+ function firstDifference(a, b) {
1113
+ const len = Math.min(a.length, b.length);
1114
+ for (let i = 0; i < len; i += 1) {
1115
+ if (a[i] !== b[i]) return { index: i, a: a.slice(Math.max(0, i - 12), i + 12), b: b.slice(Math.max(0, i - 12), i + 12) };
1116
+ }
1117
+ return { index: len, a: a.slice(len), b: b.slice(len) };
1118
+ }
1119
+
1120
+ // src/store.mjs
1121
+ import fs3 from "node:fs";
1122
+ import path3 from "node:path";
1123
+ var stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1124
+ function savePayload({ provider, payload, body, meta }) {
1125
+ fs3.mkdirSync(OUT_DIR, { recursive: true });
1126
+ const name = `${stamp()}__${provider}.json`;
1127
+ const file = path3.join(OUT_DIR, name);
1128
+ const record = {
1129
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1130
+ provider,
1131
+ meta,
1132
+ payload,
1133
+ // the exact string that must be signed and sent — never re-serialize this
1134
+ body
1135
+ };
1136
+ fs3.writeFileSync(file, JSON.stringify(record, null, 2) + "\n");
1137
+ fs3.writeFileSync(path3.join(OUT_DIR, "last.json"), JSON.stringify(record, null, 2) + "\n");
1138
+ return file;
1139
+ }
1140
+ function loadLast() {
1141
+ const file = path3.join(OUT_DIR, "last.json");
1142
+ if (!fs3.existsSync(file)) return null;
1143
+ return JSON.parse(fs3.readFileSync(file, "utf8"));
1144
+ }
1145
+ function readRecord(dir, file) {
1146
+ const full = path3.join(dir, file);
1147
+ try {
1148
+ const rec = JSON.parse(fs3.readFileSync(full, "utf8"));
1149
+ if (!rec || typeof rec !== "object" || !rec.savedAt || !rec.provider || !rec.body) return null;
1150
+ return {
1151
+ file,
1152
+ path: full,
1153
+ provider: String(rec.provider),
1154
+ savedAt: String(rec.savedAt),
1155
+ meta: rec.meta ?? {}
1156
+ };
1157
+ } catch {
1158
+ return null;
1159
+ }
1160
+ }
1161
+ function listPayloads(limit = 20) {
1162
+ if (!fs3.existsSync(OUT_DIR)) return [];
1163
+ return fs3.readdirSync(OUT_DIR).filter((f) => f.endsWith(".json") && f !== "last.json").sort().reverse().map((f) => readRecord(OUT_DIR, f)).filter(Boolean).slice(0, limit);
1164
+ }
1165
+ function loadPayload(file) {
1166
+ const full = path3.isAbsolute(file) ? file : path3.join(OUT_DIR, file);
1167
+ return JSON.parse(fs3.readFileSync(full, "utf8"));
1168
+ }
1169
+
1170
+ // src/core/build.mjs
1171
+ function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone }) {
1172
+ const provider = getProvider(providerId);
1173
+ const resolvedPhone = phone ?? cfg.customer.phone;
1174
+ const parsed = toE164(resolvedPhone, cfg.customer.countryCode);
1175
+ const payload = provider.buildPayload({
1176
+ config: cfg,
1177
+ items,
1178
+ discount,
1179
+ phone: parsed.ok ? parsed.value : String(resolvedPhone ?? "")
1180
+ });
1181
+ const fields = (provider.editableFields ?? []).map((f) => ({ ...f, value: getPath(payload, f.path) }));
1182
+ return { provider, payload, fields };
1183
+ }
1184
+ function applyEdits(payload, fields, edits) {
1185
+ for (const field of fields) {
1186
+ if (!(field.path in edits)) continue;
1187
+ const raw = edits[field.path];
1188
+ const value = field.type === "number" ? Number.parseFloat(raw) || 0 : raw;
1189
+ setPath(payload, field.path, value);
1190
+ }
1191
+ return payload;
1192
+ }
1193
+ async function buildPayload2({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, checkImageUrls = true, payload: prebuilt }) {
1194
+ if (!items?.length) throw new ConfigError("no products selected");
1195
+ const provider = getProvider(providerId);
1196
+ const resolvedPhone = phone ?? cfg.customer.phone;
1197
+ const parsed = toE164(resolvedPhone, cfg.customer.countryCode);
1198
+ if (!parsed.ok) throw new ConfigError(`phone is unusable: ${parsed.reason}`);
1199
+ const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value });
1200
+ const body = JSON.stringify(payload);
1201
+ const report = await runAll({ payload, body, provider, config: cfg, checkImageUrls });
1202
+ const endpoint = provider.webhookUrl(cfg);
1203
+ let host = "";
1204
+ try {
1205
+ host = endpoint ? new URL(endpoint).host : "";
1206
+ } catch {
1207
+ host = "";
1208
+ }
1209
+ const meta = {
1210
+ provider: provider.id,
1211
+ endpoint,
1212
+ environment: host,
1213
+ store: cfg.shopify.domain,
1214
+ 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,
1217
+ valid: report.ok
1218
+ };
1219
+ const file = savePayload({ provider: provider.id, payload, body, meta });
1220
+ audit("payload.built", { provider: provider.id, valid: report.ok, file });
1221
+ return { provider, payload, body, report, file, meta };
1222
+ }
1223
+
1224
+ // src/core/send.mjs
1225
+ import fs4 from "node:fs";
1226
+ import path4 from "node:path";
1227
+ var PROD_PATTERNS = [/\bprod(uction)?\b/i, /\blive\b/i];
1228
+ function looksLikeProduction(origin) {
1229
+ return PROD_PATTERNS.some((re) => re.test(String(origin)));
1230
+ }
1231
+ function buildRequest({ provider, config, body, timestamp }) {
1232
+ const url = provider.webhookUrl(config);
1233
+ const secret = provider.webhookSecret ? provider.webhookSecret(config) : "";
1234
+ const signed = provider.sign({ body, secret, timestamp });
1235
+ return { url, headers: signed.headers, signature: signed.signature, timestamp: signed.timestamp, body };
1236
+ }
1237
+ function toCurl({ url, headers, bodyFile }) {
1238
+ const lines = [`curl -X POST '${url}' \\`];
1239
+ for (const [k, v] of Object.entries(headers)) {
1240
+ lines.push(` -H '${k}: ${v}' \\`);
1241
+ }
1242
+ lines.push(` --data-binary @${bodyFile}`);
1243
+ return lines.join("\n");
1244
+ }
1245
+ function writeBodyFile(body, name = "last-body.json") {
1246
+ fs4.mkdirSync(STATE_DIR, { recursive: true });
1247
+ const file = path4.join(STATE_DIR, name);
1248
+ fs4.writeFileSync(file, body);
1249
+ return file;
1250
+ }
1251
+ async function send({ url, headers, body, timeoutMs = 2e4 }) {
1252
+ const controller = new AbortController();
1253
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1254
+ const startedAt = Date.now();
1255
+ try {
1256
+ const res = await fetch(url, { method: "POST", headers, body, signal: controller.signal });
1257
+ clearTimeout(timer);
1258
+ const text = await res.text();
1259
+ const result = {
1260
+ ok: res.ok,
1261
+ status: res.status,
1262
+ durationMs: Date.now() - startedAt,
1263
+ body: text,
1264
+ headers: Object.fromEntries(res.headers.entries())
1265
+ };
1266
+ audit("webhook.sent", { url, status: res.status, durationMs: result.durationMs });
1267
+ return result;
1268
+ } catch (err) {
1269
+ clearTimeout(timer);
1270
+ const failure = {
1271
+ ok: false,
1272
+ status: 0,
1273
+ durationMs: Date.now() - startedAt,
1274
+ error: err.name === "AbortError" ? `timed out after ${timeoutMs}ms` : err.message
1275
+ };
1276
+ audit("webhook.failed", { url, error: failure.error });
1277
+ return failure;
1278
+ }
1279
+ }
1280
+
1281
+ // src/core/dispatch.mjs
1282
+ async function dispatch({ cfg, record, force = false, dryRun = false, revalidate = true }) {
1283
+ const rec = record ?? loadLast();
1284
+ if (!rec) throw new ForgeError("nothing to send", { hint: "build a payload first" });
1285
+ const provider = getProvider(rec.provider);
1286
+ let report = null;
1287
+ if (revalidate) {
1288
+ report = await runAll({ payload: rec.payload, body: rec.body, provider, config: cfg, checkImageUrls: false });
1289
+ 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`
1292
+ });
1293
+ }
1294
+ }
1295
+ const request = buildRequest({ provider, config: cfg, body: rec.body });
1296
+ if (looksLikeProduction(request.url) && !force) {
1297
+ throw new ForgeError(`target looks like production: ${request.url}`, { hint: "pass --force if you really mean it" });
1298
+ }
1299
+ 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 };
1302
+ const result = await send({ url: request.url, headers: request.headers, body: rec.body });
1303
+ return { ...result, dryRun: false, request, bodyFile, curl, report, provider };
1304
+ }
1305
+ function explainStatus(status) {
1306
+ if (status >= 200 && status < 300) return "accepted \u2014 a 2xx only means it was received; check eventData to confirm the event landed";
1307
+ if (status === 401 || status === 403) return "signature rejected \u2014 the stored webhook_secret does not match the one used to sign";
1308
+ if (status === 404) return "not found \u2014 wrong company id or urlKey in the webhook URL";
1309
+ if (status === 400) return "bad request \u2014 the consumer could not parse the payload";
1310
+ if (status >= 500) return "receiver error \u2014 check the endpoint logs";
1311
+ return "unexpected status";
1312
+ }
1313
+
1314
+ // src/core/clipboard.mjs
1315
+ import { spawn } from "node:child_process";
1316
+ var CANDIDATES = [
1317
+ { cmd: "wl-copy", args: [], platforms: ["linux"] },
1318
+ { cmd: "xclip", args: ["-selection", "clipboard"], platforms: ["linux"] },
1319
+ { cmd: "xsel", args: ["--clipboard", "--input"], platforms: ["linux"] },
1320
+ { cmd: "pbcopy", args: [], platforms: ["darwin"] },
1321
+ { cmd: "clip.exe", args: [], platforms: ["linux", "win32"] },
1322
+ { cmd: "clip", args: [], platforms: ["win32"] }
1323
+ ];
1324
+ function pipeTo(cmd, args, text) {
1325
+ return new Promise((resolve) => {
1326
+ let child;
1327
+ try {
1328
+ child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
1329
+ } catch {
1330
+ return resolve(false);
1331
+ }
1332
+ child.on("error", () => resolve(false));
1333
+ child.on("close", (code) => resolve(code === 0));
1334
+ try {
1335
+ child.stdin.end(text);
1336
+ } catch {
1337
+ resolve(false);
1338
+ }
1339
+ });
1340
+ }
1341
+ function osc52(text, stream) {
1342
+ const out = stream ?? (process.stderr.isTTY ? process.stderr : process.stdout);
1343
+ try {
1344
+ const encoded = Buffer.from(text, "utf8").toString("base64");
1345
+ out.write(`\x1B]52;c;${encoded}\x07`);
1346
+ return true;
1347
+ } catch {
1348
+ return false;
1349
+ }
1350
+ }
1351
+ async function copy(text, { allowOsc52 = true } = {}) {
1352
+ const platform = process.platform;
1353
+ for (const candidate of CANDIDATES) {
1354
+ if (!candidate.platforms.includes(platform)) continue;
1355
+ const ok = await pipeTo(candidate.cmd, candidate.args, text);
1356
+ if (ok) return { ok: true, via: candidate.cmd };
1357
+ }
1358
+ if (allowOsc52 && (process.stderr.isTTY || process.stdout.isTTY) && osc52(text)) {
1359
+ return { ok: true, via: "terminal (OSC 52)" };
1360
+ }
1361
+ return { ok: false, via: "none" };
1362
+ }
1363
+
1364
+ // src/ui/screens/Build.jsx
1365
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1366
+ var STEP = {
1367
+ LOADING: "loading",
1368
+ PICK: "pick",
1369
+ VARIANT: "variant",
1370
+ QUANTITY: "quantity",
1371
+ DISCOUNT: "discount",
1372
+ MODE: "mode",
1373
+ FIELDS: "fields",
1374
+ BUILDING: "building",
1375
+ REPORT: "report",
1376
+ CONFIRM: "confirm",
1377
+ SENDING: "sending",
1378
+ RESULT: "result",
1379
+ ERROR: "error"
1380
+ };
1381
+ function Build({ config, providerId = "cashfree-occ", onDone }) {
1382
+ const { exit } = useApp();
1383
+ const [step, setStep] = useState(STEP.LOADING);
1384
+ const [products, setProducts] = useState([]);
1385
+ const [chosen, setChosen] = useState([]);
1386
+ const [cursor, setCursor] = useState(0);
1387
+ const [items, setItems] = useState([]);
1388
+ const [pendingVariant, setPendingVariant] = useState(null);
1389
+ const [draft, setDraft] = useState(null);
1390
+ const [fieldIndex, setFieldIndex] = useState(0);
1391
+ const [edits, setEdits] = useState({});
1392
+ const [built, setBuilt] = useState(null);
1393
+ const [copied, setCopied] = useState(null);
1394
+ const [showPayload, setShowPayload] = useState(true);
1395
+ const [sendResult, setSendResult] = useState(null);
1396
+ const [error, setError] = useState(null);
1397
+ useInput3((input, key) => {
1398
+ if (key.escape) onDone();
1399
+ 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
+ }
1404
+ });
1405
+ useEffect(() => {
1406
+ let alive = true;
1407
+ fetchCatalogue(config).then((list) => {
1408
+ if (!alive) return;
1409
+ if (!list.length) {
1410
+ setError({ message: "Shopify returned no products", hint: "check that the token has read_products and the store has active products" });
1411
+ setStep(STEP.ERROR);
1412
+ return;
1413
+ }
1414
+ setProducts(list);
1415
+ setStep(STEP.PICK);
1416
+ }).catch((err) => {
1417
+ if (!alive) return;
1418
+ setError({ message: err.message, hint: err.hint });
1419
+ setStep(STEP.ERROR);
1420
+ });
1421
+ return () => {
1422
+ alive = false;
1423
+ };
1424
+ }, []);
1425
+ const advance = (nextItems, nextCursor) => {
1426
+ if (nextCursor >= chosen.length) {
1427
+ setItems(nextItems);
1428
+ setStep(STEP.DISCOUNT);
1429
+ return;
1430
+ }
1431
+ setItems(nextItems);
1432
+ setCursor(nextCursor);
1433
+ const product = chosen[nextCursor];
1434
+ if (product.variants.length > 1) {
1435
+ setStep(STEP.VARIANT);
1436
+ } else {
1437
+ setPendingVariant(product.variants[0]);
1438
+ setStep(STEP.QUANTITY);
1439
+ }
1440
+ };
1441
+ const onProductsPicked = (ids) => {
1442
+ const picked = ids.map((id) => products.find((p) => p.id === id)).filter(Boolean);
1443
+ if (!picked.length) return;
1444
+ setChosen(picked);
1445
+ const first = picked[0];
1446
+ setCursor(0);
1447
+ setItems([]);
1448
+ if (first.variants.length > 1) setStep(STEP.VARIANT);
1449
+ else {
1450
+ setPendingVariant(first.variants[0]);
1451
+ setStep(STEP.QUANTITY);
1452
+ }
1453
+ };
1454
+ const onVariantPicked = (variantId) => {
1455
+ const product = chosen[cursor];
1456
+ setPendingVariant(product.variants.find((v) => v.id === variantId) ?? product.variants[0]);
1457
+ setStep(STEP.QUANTITY);
1458
+ };
1459
+ const onQuantity = (raw) => {
1460
+ const qty = Math.max(1, Number.parseInt(raw, 10) || 1);
1461
+ const product = chosen[cursor];
1462
+ const nextItems = [...items, { product, variant: pendingVariant, quantity: qty }];
1463
+ advance(nextItems, cursor + 1);
1464
+ };
1465
+ const onDiscount = (raw) => {
1466
+ const discount = Math.max(0, Number.parseFloat(raw) || 0);
1467
+ try {
1468
+ const next = draftPayload({ cfg: config, providerId, items, discount });
1469
+ setDraft({ ...next, discount });
1470
+ setStep(STEP.MODE);
1471
+ } catch (err) {
1472
+ setError({ message: err.message, hint: err.hint });
1473
+ setStep(STEP.ERROR);
1474
+ }
1475
+ };
1476
+ const finalise = (payload) => {
1477
+ setStep(STEP.BUILDING);
1478
+ buildPayload2({ cfg: config, providerId, items, discount: draft.discount, payload }).then(async (result) => {
1479
+ setBuilt(result);
1480
+ if (config.defaults.copyToClipboard !== false) {
1481
+ setCopied(await copy(result.body));
1482
+ }
1483
+ setStep(STEP.REPORT);
1484
+ }).catch((err) => {
1485
+ setError({ message: err.message, hint: err.hint });
1486
+ setStep(STEP.ERROR);
1487
+ });
1488
+ };
1489
+ const onMode = (mode) => {
1490
+ if (mode === "accept") return finalise(draft.payload);
1491
+ setFieldIndex(0);
1492
+ setEdits({});
1493
+ setStep(STEP.FIELDS);
1494
+ };
1495
+ const onFieldSubmit = (raw) => {
1496
+ const field = draft.fields[fieldIndex];
1497
+ const nextEdits = { ...edits, [field.path]: raw };
1498
+ setEdits(nextEdits);
1499
+ if (fieldIndex + 1 < draft.fields.length) {
1500
+ setFieldIndex(fieldIndex + 1);
1501
+ return;
1502
+ }
1503
+ finalise(applyEdits(draft.payload, draft.fields, nextEdits));
1504
+ };
1505
+ const doSend = () => {
1506
+ setStep(STEP.SENDING);
1507
+ dispatch({ cfg: config, record: { provider: built.provider.id, payload: built.payload, body: built.body } }).then((result) => {
1508
+ setSendResult(result);
1509
+ setStep(STEP.RESULT);
1510
+ }).catch((err) => {
1511
+ setError({ message: err.message, hint: err.hint });
1512
+ setStep(STEP.ERROR);
1513
+ });
1514
+ };
1515
+ const store = config.shopify.domain;
1516
+ const currency = config.defaults.currency;
1517
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1518
+ /* @__PURE__ */ jsx7(
1519
+ Header,
1520
+ {
1521
+ title: "Build payload",
1522
+ subtitle: `${metaFor(providerId).label} \xB7 abandoned checkout`,
1523
+ right: store
1524
+ }
1525
+ ),
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: [
1529
+ `Select the products in the abandoned cart `,
1530
+ /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "(space toggles \xB7 enter confirms)" })
1531
+ ] }),
1532
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
1533
+ MultiSelect,
1534
+ {
1535
+ visibleOptionCount: 10,
1536
+ options: products.map((p) => ({
1537
+ label: `${truncate(p.title, 44).padEnd(45)} ${money(p.variants[0]?.price, currency)}`,
1538
+ value: p.id
1539
+ })),
1540
+ onSubmit: onProductsPicked
1541
+ }
1542
+ ) })
1543
+ ] }),
1544
+ step === STEP.VARIANT && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1545
+ /* @__PURE__ */ jsxs7(Text7, { children: [
1546
+ `Variant for `,
1547
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: palette.accent, children: chosen[cursor]?.title })
1548
+ ] }),
1549
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
1550
+ Select4,
1551
+ {
1552
+ visibleOptionCount: 8,
1553
+ options: chosen[cursor].variants.map((v) => ({
1554
+ label: `${truncate(v.title, 34).padEnd(35)} ${money(v.price, currency)}${v.sku ? " " + v.sku : ""}`,
1555
+ value: v.id
1556
+ })),
1557
+ onChange: onVariantPicked
1558
+ },
1559
+ `variant-${cursor}`
1560
+ ) })
1561
+ ] }),
1562
+ step === STEP.QUANTITY && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1563
+ /* @__PURE__ */ jsxs7(Text7, { children: [
1564
+ `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}` : "" })
1567
+ ] }),
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}`)
1571
+ ] })
1572
+ ] }),
1573
+ step === STEP.DISCOUNT && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1574
+ /* @__PURE__ */ jsxs7(Text7, { children: [
1575
+ `Cart discount in ${currency} `,
1576
+ /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "(0 for none)" })
1577
+ ] }),
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 })
1581
+ ] })
1582
+ ] }),
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." })
1587
+ ] }),
1588
+ /* @__PURE__ */ jsx7(
1589
+ Select4,
1590
+ {
1591
+ options: [
1592
+ { label: "Use the suggested values".padEnd(30) + "fastest", value: "accept" },
1593
+ { label: "Review every field".padEnd(30) + "enter accepts \xB7 type to change", value: "review" }
1594
+ ],
1595
+ onChange: onMode
1596
+ }
1597
+ )
1598
+ ] }),
1599
+ step === STEP.FIELDS && draft && /* @__PURE__ */ jsx7(
1600
+ FieldPrompt,
1601
+ {
1602
+ field: draft.fields[fieldIndex],
1603
+ index: fieldIndex,
1604
+ total: draft.fields.length,
1605
+ value: edits[draft.fields[fieldIndex].path] ?? draft.fields[fieldIndex].value,
1606
+ onSubmit: onFieldSubmit
1607
+ }
1608
+ ),
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,
1623
+ {
1624
+ isDisabled: !built.report.ok,
1625
+ onConfirm: doSend,
1626
+ onCancel: () => onDone()
1627
+ }
1628
+ )
1629
+ ] }),
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" })
1631
+ ] }),
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) })
1638
+ ] }),
1639
+ /* @__PURE__ */ jsx7(
1640
+ KeyValue,
1641
+ {
1642
+ rows: [
1643
+ ["url", sendResult.request.url],
1644
+ ["timestamp", sendResult.request.timestamp],
1645
+ ["signature", truncate(sendResult.request.signature, 44)],
1646
+ ["took", `${sendResult.durationMs}ms`],
1647
+ ["response", truncate(String(sendResult.body ?? "\u2014"), 60)],
1648
+ ["payload", built.file]
1649
+ ]
1650
+ }
1651
+ ),
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 })
1655
+ ] }),
1656
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: "press enter to return to the menu" }) })
1657
+ ] }),
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" }) })
1662
+ ] })
1663
+ ] });
1664
+ }
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(
1674
+ KeyValue,
1675
+ {
1676
+ keyWidth: 22,
1677
+ rows: [
1678
+ ["POST", truncate(request.url, 56)],
1679
+ ["x-webhook-timestamp", request.timestamp],
1680
+ ["x-webhook-signature", truncate(request.signature, 46)],
1681
+ ["body", `${built.body.length} bytes`]
1682
+ ]
1683
+ }
1684
+ );
1685
+ }
1686
+ function FieldPrompt({ field, index, total, value, onSubmit }) {
1687
+ 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}` })
1693
+ ] }),
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
1698
+ ] }),
1699
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1700
+ /* @__PURE__ */ jsx7(Text7, { color: palette.accent, children: "\u276F " }),
1701
+ /* @__PURE__ */ jsx7(
1702
+ TextInput,
1703
+ {
1704
+ defaultValue: value == null ? "" : String(value),
1705
+ placeholder: field.optional ? "(leave blank)" : "",
1706
+ onSubmit
1707
+ },
1708
+ field.path
1709
+ )
1710
+ ] }),
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" }) })
1712
+ ] });
1713
+ }
1714
+ 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
+ ) });
1730
+ }
1731
+
1732
+ // 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";
1735
+ 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";
1737
+ var FIELDS = [
1738
+ { group: "Customer", key: "customer.firstName", label: "First name", placeholder: "Test" },
1739
+ { group: "Customer", key: "customer.lastName", label: "Last name", placeholder: "Shopper" },
1740
+ { group: "Customer", key: "customer.email", label: "Email" },
1741
+ { group: "Customer", key: "customer.countryCode", label: "Country code", placeholder: "IN", transform: (v) => v.toUpperCase() },
1742
+ { group: "Customer", key: "customer.phone", label: "Phone", hint: "the handset that will receive the message" },
1743
+ { group: "Customer", key: "customer.address1", label: "Address line 1" },
1744
+ { group: "Customer", key: "customer.address2", label: "Address line 2", optional: true },
1745
+ { group: "Customer", key: "customer.city", label: "City" },
1746
+ { group: "Customer", key: "customer.province", label: "Province / state" },
1747
+ { group: "Customer", key: "customer.provinceCode", label: "Province code", optional: true },
1748
+ { group: "Customer", key: "customer.country", label: "Country name", placeholder: "India" },
1749
+ { group: "Customer", key: "customer.zip", label: "Zip / postcode" },
1750
+ { group: "Defaults", key: "defaults.currency", label: "Currency", placeholder: "INR", transform: (v) => v.toUpperCase() },
1751
+ { group: "Defaults", key: "defaults.platform", label: "Platform", placeholder: "shopify" }
1752
+ ];
1753
+ var SECTION_HINTS = {
1754
+ Customer: "the shopper the abandoned cart belongs to",
1755
+ Defaults: "currency, platform"
1756
+ };
1757
+ var get = (obj, path6) => path6.split(".").reduce((a, k) => a == null ? void 0 : a[k], obj);
1758
+ var set = (obj, path6, value) => {
1759
+ const keys = path6.split(".");
1760
+ const last = keys.pop();
1761
+ const target = keys.reduce((a, k) => a[k] ??= {}, obj);
1762
+ target[last] = value;
1763
+ return obj;
1764
+ };
1765
+ function Configure({ config, section: initialSection, onDone }) {
1766
+ const [draft] = useState2(() => structuredClone(config));
1767
+ const [section, setSection] = useState2(initialSection ?? null);
1768
+ const [index, setIndex] = useState2(0);
1769
+ const [phase, setPhase] = useState2("form");
1770
+ const [verifyMsg, setVerifyMsg] = useState2(null);
1771
+ const fields = section ? FIELDS.filter((f) => f.group.toLowerCase() === section.toLowerCase()) : [];
1772
+ useInput4((input, key) => {
1773
+ if (key.escape) onDone();
1774
+ if (phase === "done" && (key.return || input === "q")) onDone(draft);
1775
+ });
1776
+ const field = fields[index];
1777
+ const commit = (raw) => {
1778
+ const value = field.transform ? field.transform(String(raw ?? "").trim()) : String(raw ?? "").trim();
1779
+ set(draft, field.key, value);
1780
+ if (field.key === "customer.phone") {
1781
+ const parsed = toE164(value, get(draft, "customer.countryCode"));
1782
+ if (parsed.ok) {
1783
+ set(draft, "customer.phone", parsed.value);
1784
+ if (!draft.allowedPhones.includes(parsed.value)) draft.allowedPhones.push(parsed.value);
1785
+ }
1786
+ }
1787
+ if (index + 1 < fields.length) {
1788
+ setIndex(index + 1);
1789
+ return;
1790
+ }
1791
+ finish();
1792
+ };
1793
+ const finish = () => {
1794
+ {
1795
+ const file = saveConfig(draft);
1796
+ setVerifyMsg({ ok: true, text: `${section} settings saved`, file });
1797
+ setPhase("done");
1798
+ return;
1799
+ }
1800
+ setPhase("verifying");
1801
+ verifyStore(draft).then(({ shop, count }) => {
1802
+ if (shop?.currency) draft.defaults.currency = shop.currency;
1803
+ setVerifyMsg({ ok: true, text: `connected to ${shop?.name} \xB7 ${shop?.currency} \xB7 ${count ?? "?"} products` });
1804
+ }).catch((err) => setVerifyMsg({ ok: false, text: err.message, hint: err.hint })).finally(() => {
1805
+ const file = saveConfig(draft);
1806
+ setVerifyMsg((m) => ({ ...m, file }));
1807
+ setPhase("done");
1808
+ });
1809
+ };
1810
+ 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(
1814
+ Select5,
1815
+ {
1816
+ visibleOptionCount: 8,
1817
+ options: Object.entries(SECTION_HINTS).map(([key, hint]) => ({
1818
+ label: `${key.padEnd(10)} ${hint}`,
1819
+ value: key
1820
+ })),
1821
+ onChange: (value) => {
1822
+ setSection(value);
1823
+ setIndex(0);
1824
+ }
1825
+ }
1826
+ )
1827
+ ] });
1828
+ }
1829
+ 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" })
1833
+ ] });
1834
+ }
1835
+ 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" }) })
1842
+ ] });
1843
+ }
1844
+ 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(
1853
+ Select5,
1854
+ {
1855
+ options: field.options,
1856
+ defaultValue: current || void 0,
1857
+ onChange: commit
1858
+ },
1859
+ 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(
1861
+ TextInput2,
1862
+ {
1863
+ defaultValue: current,
1864
+ placeholder: field.placeholder ?? (field.optional ? "(optional)" : ""),
1865
+ onSubmit: (v) => commit(v || current)
1866
+ },
1867
+ field.key
1868
+ )
1869
+ ] })
1870
+ ] })
1871
+ ] });
1872
+ }
1873
+ function Progress({ index, total }) {
1874
+ 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}` })
1879
+ ] });
1880
+ }
1881
+
1882
+ // 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";
1885
+ 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";
1887
+ var STEP2 = { DOMAIN: "domain", TOKEN: "token", VERIFY: "verify", DONE: "done", FAILED: "failed" };
1888
+ function ConnectShopify({ config, onDone }) {
1889
+ const [draft] = useState3(() => structuredClone(config));
1890
+ const [step, setStep] = useState3(STEP2.DOMAIN);
1891
+ const [result, setResult] = useState3(null);
1892
+ const [error, setError] = useState3(null);
1893
+ useInput5((input, key) => {
1894
+ if (key.escape) onDone();
1895
+ if (step === STEP2.DONE && (key.return || input === "q")) onDone(draft);
1896
+ if (step === STEP2.FAILED && (key.return || input === "q")) setStep(STEP2.DOMAIN);
1897
+ });
1898
+ const onDomain = (value) => {
1899
+ draft.shopify.domain = normalizeDomain(value || draft.shopify.domain);
1900
+ if (!draft.shopify.domain) return;
1901
+ setStep(STEP2.TOKEN);
1902
+ };
1903
+ const onToken = (value) => {
1904
+ draft.shopify.accessToken = value || draft.shopify.accessToken;
1905
+ if (!draft.shopify.accessToken) return;
1906
+ setStep(STEP2.VERIFY);
1907
+ verifyStore(draft).then(({ shop, count }) => {
1908
+ const applied = applyShopDefaults(draft, shop, count);
1909
+ setResult({ shop, count, applied });
1910
+ saveConfig(draft);
1911
+ setStep(STEP2.DONE);
1912
+ }).catch((err) => {
1913
+ setError({ message: err.message, hint: err.hint });
1914
+ setStep(STEP2.FAILED);
1915
+ });
1916
+ };
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(
1924
+ TextInput3,
1925
+ {
1926
+ defaultValue: draft.shopify.domain,
1927
+ placeholder: "your-store.myshopify.com",
1928
+ onSubmit: onDomain
1929
+ }
1930
+ )
1931
+ ] })
1932
+ ] }),
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(
1939
+ PasswordInput2,
1940
+ {
1941
+ placeholder: draft.shopify.accessToken ? mask(draft.shopify.accessToken) : "shpat_\u2026",
1942
+ onSubmit: onToken
1943
+ }
1944
+ )
1945
+ ] })
1946
+ ] }),
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 })
1953
+ ] }),
1954
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx9(
1955
+ KeyValue,
1956
+ {
1957
+ keyWidth: 16,
1958
+ rows: [
1959
+ ["domain", result.shop?.myshopify_domain ?? draft.shopify.domain],
1960
+ ["storefront", result.shop?.domain ?? "\u2014"],
1961
+ ["products", String(result.count ?? "?")],
1962
+ ["currency", result.shop?.currency ?? "\u2014"],
1963
+ ["country", `${result.shop?.country_name ?? "\u2014"} (${result.shop?.country_code ?? "\u2014"})`],
1964
+ ["timezone", result.shop?.iana_timezone ?? "\u2014"],
1965
+ ["plan", result.shop?.plan_display_name ?? "\u2014"],
1966
+ ["contact", result.shop?.email ?? "\u2014"]
1967
+ ]
1968
+ }
1969
+ ) }),
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" }) })
1973
+ ] }),
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" })
1982
+ ] }),
1983
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: palette.dim, children: "press enter to try again" }) })
1984
+ ] })
1985
+ ] });
1986
+ }
1987
+
1988
+ // 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";
1991
+ 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";
1993
+ var STEP3 = { PICK: "pick", URL: "url", SECRET: "secret", PROBE: "probe", DONE: "done" };
1994
+ function Webhook({ config, onDone }) {
1995
+ const [draft] = useState4(() => structuredClone(config));
1996
+ const [providerId, setProviderId] = useState4(null);
1997
+ const [step, setStep] = useState4(STEP3.PICK);
1998
+ const [probe, setProbe] = useState4(null);
1999
+ const [urlError, setUrlError] = useState4(null);
2000
+ useInput6((input, key) => {
2001
+ if (key.escape) {
2002
+ if (step === STEP3.PICK) onDone(draft);
2003
+ else {
2004
+ setStep(STEP3.PICK);
2005
+ setProviderId(null);
2006
+ }
2007
+ }
2008
+ if (step === STEP3.DONE && (key.return || input === "q")) {
2009
+ setStep(STEP3.PICK);
2010
+ setProviderId(null);
2011
+ }
2012
+ });
2013
+ const meta = providerId ? metaFor(providerId) : null;
2014
+ const target = providerId ? targetFor(draft, providerId) : null;
2015
+ const onUrl = (value) => {
2016
+ const url = (value || target.webhookUrl || "").trim();
2017
+ if (!/^https?:\/\/.+/i.test(url)) {
2018
+ setUrlError("must be a full URL starting with http:// or https://");
2019
+ return;
2020
+ }
2021
+ setUrlError(null);
2022
+ setTarget(draft, providerId, { webhookUrl: url });
2023
+ setStep(STEP3.SECRET);
2024
+ };
2025
+ const onSecret = (value) => {
2026
+ setTarget(draft, providerId, { webhookSecret: value || target.webhookSecret });
2027
+ saveConfig(draft);
2028
+ setStep(STEP3.PROBE);
2029
+ probeEndpoint(targetFor(draft, providerId).webhookUrl).then(setProbe).finally(() => setStep(STEP3.DONE));
2030
+ };
2031
+ if (step === STEP3.PICK) {
2032
+ const options = [
2033
+ ...listProviderMeta().map((p) => {
2034
+ const t = targetFor(draft, p.id);
2035
+ return {
2036
+ label: p.label.padEnd(30) + (t.webhookUrl ? truncate(t.webhookUrl, 34) : "not set"),
2037
+ value: p.id
2038
+ };
2039
+ }),
2040
+ { label: "Back".padEnd(30), value: "__back" }
2041
+ ];
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(
2045
+ Select6,
2046
+ {
2047
+ visibleOptionCount: options.length,
2048
+ options,
2049
+ onChange: (v) => {
2050
+ if (v === "__back") return onDone(draft);
2051
+ setProviderId(v);
2052
+ setUrlError(null);
2053
+ setStep(STEP3.URL);
2054
+ }
2055
+ }
2056
+ ),
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` }) })
2058
+ ] });
2059
+ }
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(
2068
+ TextInput4,
2069
+ {
2070
+ defaultValue: target.webhookUrl,
2071
+ placeholder: "https://your-endpoint.example.com/webhook/\u2026",
2072
+ onSubmit: onUrl
2073
+ }
2074
+ )
2075
+ ] })
2076
+ ] }),
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(
2083
+ PasswordInput3,
2084
+ {
2085
+ placeholder: target.webhookSecret ? mask(target.webhookSecret) : "the signing secret\u2026",
2086
+ onSubmit: onSecret
2087
+ }
2088
+ )
2089
+ ] })
2090
+ ] }),
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" })
2097
+ ] }),
2098
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", borderStyle: "round", borderColor: palette.ok, paddingX: 1, children: /* @__PURE__ */ jsx10(
2099
+ KeyValue,
2100
+ {
2101
+ keyWidth: 10,
2102
+ rows: [
2103
+ ["provider", meta.label],
2104
+ ["url", truncate(targetFor(draft, providerId).webhookUrl, 58)],
2105
+ ["secret", targetFor(draft, providerId).webhookSecret ? mask(targetFor(draft, providerId).webhookSecret) : "none \u2014 signature will be skipped"]
2106
+ ]
2107
+ }
2108
+ ) }),
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" }) })
2111
+ ] })
2112
+ ] });
2113
+ }
2114
+ async function probeEndpoint(url) {
2115
+ const controller = new AbortController();
2116
+ const timer = setTimeout(() => controller.abort(), 1e4);
2117
+ try {
2118
+ const res = await fetch(url, {
2119
+ method: "POST",
2120
+ headers: { "content-type": "application/json" },
2121
+ body: JSON.stringify({ type: "FORGE_PROBE" }),
2122
+ signal: controller.signal
2123
+ });
2124
+ clearTimeout(timer);
2125
+ return { reachable: true, status: res.status };
2126
+ } catch (err) {
2127
+ clearTimeout(timer);
2128
+ return { reachable: false, error: err.name === "AbortError" ? "timed out" : err.message };
2129
+ }
2130
+ }
2131
+
2132
+ // 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";
2135
+ import { Spinner as Spinner5 } from "@inkjs/ui";
2136
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2137
+ function Doctor({ config, onDone }) {
2138
+ const [checks, setChecks] = useState5(null);
2139
+ useInput7((input, key) => {
2140
+ if (key.escape || key.return || input === "q") onDone();
2141
+ });
2142
+ useEffect2(() => {
2143
+ let alive = true;
2144
+ run(config).then((result) => alive && setChecks(result));
2145
+ return () => {
2146
+ alive = false;
2147
+ };
2148
+ }, []);
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" }) })
2156
+ ] })
2157
+ ] });
2158
+ }
2159
+ async function run(config) {
2160
+ const cfgChecks = configStatus(config).map((row) => ({
2161
+ name: row.key,
2162
+ ok: row.ok,
2163
+ detail: row.value ? row.secret ? mask(row.value) : row.value : "not set"
2164
+ }));
2165
+ const connectivity = [];
2166
+ if (config.shopify.domain && config.shopify.accessToken) {
2167
+ try {
2168
+ const { shop, count } = await verifyStore(config);
2169
+ connectivity.push({ name: "Shopify", ok: true, detail: `${shop?.name} \xB7 ${shop?.currency} \xB7 ${count ?? "?"} products` });
2170
+ } catch (err) {
2171
+ connectivity.push({ name: "Shopify", ok: false, detail: err.message });
2172
+ }
2173
+ } else {
2174
+ connectivity.push({ name: "Shopify", ok: false, detail: "not configured" });
2175
+ }
2176
+ const url = getProvider("cashfree-occ").webhookUrl(config);
2177
+ if (url) {
2178
+ try {
2179
+ const controller = new AbortController();
2180
+ const timer = setTimeout(() => controller.abort(), 12e3);
2181
+ const res = await fetch(url, {
2182
+ method: "POST",
2183
+ headers: { "content-type": "application/json" },
2184
+ body: JSON.stringify({ type: "FORGE_PROBE" }),
2185
+ signal: controller.signal
2186
+ });
2187
+ clearTimeout(timer);
2188
+ connectivity.push({ name: "Webhook endpoint", ok: true, detail: `reachable \u2014 HTTP ${res.status} (unsigned probe, rejection is expected)` });
2189
+ } catch (err) {
2190
+ connectivity.push({ name: "Webhook endpoint", ok: false, detail: err.name === "AbortError" ? "timeout" : err.message });
2191
+ }
2192
+ } else {
2193
+ connectivity.push({ name: "Webhook endpoint", ok: false, detail: "not configured" });
2194
+ }
2195
+ const runtime = [
2196
+ { name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20, detail: process.version },
2197
+ { name: "global fetch", ok: typeof fetch === "function", detail: typeof fetch === "function" ? "available" : "missing \u2014 needs node 18+" }
2198
+ ];
2199
+ return { config: cfgChecks, connectivity, runtime };
2200
+ }
2201
+
2202
+ // 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";
2205
+ 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";
2207
+ function History({ config, onDone }) {
2208
+ const [items] = useState6(() => listPayloads(20));
2209
+ const [selected, setSelected] = useState6(null);
2210
+ const [phase, setPhase] = useState6("list");
2211
+ const [result, setResult] = useState6(null);
2212
+ const [error, setError] = useState6(null);
2213
+ useInput8((input, key) => {
2214
+ if (key.escape) onDone();
2215
+ if ((phase === "result" || phase === "error") && (key.return || input === "q")) onDone();
2216
+ });
2217
+ const onPick = (file) => {
2218
+ setSelected(loadPayload(file));
2219
+ setPhase("detail");
2220
+ };
2221
+ const resend = () => {
2222
+ setPhase("sending");
2223
+ dispatch({ cfg: config, record: selected }).then((r) => {
2224
+ setResult(r);
2225
+ setPhase("result");
2226
+ }).catch((err) => {
2227
+ setError(err);
2228
+ setPhase("error");
2229
+ });
2230
+ };
2231
+ 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" })
2235
+ ] });
2236
+ }
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(
2240
+ Select7,
2241
+ {
2242
+ visibleOptionCount: 10,
2243
+ options: items.map((i) => ({
2244
+ label: `${i.savedAt.replace("T", " ").slice(0, 19)} ${i.provider} ${money(i.meta.totalPrice, i.meta.currency)} ${i.meta.valid ? "\u2714" : "\u2716"}`,
2245
+ value: i.path
2246
+ })),
2247
+ onChange: onPick
2248
+ }
2249
+ ),
2250
+ phase === "detail" && selected && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2251
+ /* @__PURE__ */ jsx12(Section, { title: "Payload", children: /* @__PURE__ */ jsx12(
2252
+ KeyValue,
2253
+ {
2254
+ rows: [
2255
+ ["saved", selected.savedAt],
2256
+ ["provider", selected.provider],
2257
+ ["environment", selected.meta?.environment ?? "\u2014"],
2258
+ ["store", selected.meta?.store ?? "\u2014"],
2259
+ ["items", (selected.meta?.items ?? []).map((i) => `${truncate(i.title, 24)} \xD7${i.qty}`).join(", ")],
2260
+ ["total", money(selected.meta?.totalPrice, selected.meta?.currency)],
2261
+ ["bytes", String(selected.body.length)]
2262
+ ]
2263
+ }
2264
+ ) }),
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") })
2268
+ ] })
2269
+ ] }),
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) })
2276
+ ] }),
2277
+ /* @__PURE__ */ jsx12(Text12, { color: palette.dim, children: "press enter to return" })
2278
+ ] }),
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" })
2283
+ ] })
2284
+ ] });
2285
+ }
2286
+
2287
+ // 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";
2290
+ import { Select as Select8, ConfirmInput as ConfirmInput3, Alert as Alert6, Badge as Badge5 } from "@inkjs/ui";
2291
+
2292
+ // src/core/cleanup.mjs
2293
+ import fs5 from "node:fs";
2294
+ import path5 from "node:path";
2295
+ var bytes = (n) => n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} kB` : `${(n / 1024 / 1024).toFixed(1)} MB`;
2296
+ function sizeOf(file) {
2297
+ try {
2298
+ return fs5.statSync(file).size;
2299
+ } catch {
2300
+ return 0;
2301
+ }
2302
+ }
2303
+ function inspect() {
2304
+ const payloadFiles = fs5.existsSync(OUT_DIR) ? fs5.readdirSync(OUT_DIR).filter((f) => f.endsWith(".json")) : [];
2305
+ const payloadBytes = payloadFiles.reduce((sum, f) => sum + sizeOf(path5.join(OUT_DIR, f)), 0);
2306
+ const logBytes = sizeOf(logPath);
2307
+ const configBytes = sizeOf(CONFIG_PATH);
2308
+ return {
2309
+ payloads: { count: payloadFiles.length, bytes: payloadBytes, label: `${payloadFiles.length} saved payload${payloadFiles.length === 1 ? "" : "s"} (${bytes(payloadBytes)})`, path: OUT_DIR },
2310
+ logs: { count: logBytes ? 1 : 0, bytes: logBytes, label: logBytes ? `activity log (${bytes(logBytes)})` : "no log file", path: logPath },
2311
+ config: { count: configBytes ? 1 : 0, bytes: configBytes, label: configBytes ? `config incl. Shopify token + webhook secret (${bytes(configBytes)})` : "no config file", path: CONFIG_PATH }
2312
+ };
2313
+ }
2314
+ function clearPayloads() {
2315
+ if (!fs5.existsSync(OUT_DIR)) return { removed: 0 };
2316
+ const files = fs5.readdirSync(OUT_DIR).filter((f) => f.endsWith(".json"));
2317
+ for (const f of files) fs5.rmSync(path5.join(OUT_DIR, f), { force: true });
2318
+ fs5.rmSync(path5.join(OUT_DIR, "last-body.json"), { force: true });
2319
+ fs5.rmSync(path5.join(STATE_DIR, "last-body.json"), { force: true });
2320
+ return { removed: files.length };
2321
+ }
2322
+ function clearLogs() {
2323
+ const existed = fs5.existsSync(logPath);
2324
+ fs5.rmSync(logPath, { force: true });
2325
+ return { removed: existed ? 1 : 0 };
2326
+ }
2327
+ function clearShopCache(cfg) {
2328
+ const had = Boolean(cfg.shopify.shop);
2329
+ cfg.shopify.shop = null;
2330
+ saveConfig(cfg);
2331
+ return { removed: had ? 1 : 0 };
2332
+ }
2333
+ function disconnectShopify(cfg) {
2334
+ cfg.shopify.domain = "";
2335
+ cfg.shopify.accessToken = "";
2336
+ cfg.shopify.shop = null;
2337
+ saveConfig(cfg);
2338
+ return { removed: 1 };
2339
+ }
2340
+ function clearConfig() {
2341
+ const existed = fs5.existsSync(CONFIG_PATH);
2342
+ fs5.rmSync(CONFIG_PATH, { force: true });
2343
+ return { removed: existed ? 1 : 0 };
2344
+ }
2345
+ function clearAll() {
2346
+ const payloads = clearPayloads();
2347
+ const logs = clearLogs();
2348
+ const config = clearConfig();
2349
+ return {
2350
+ payloads: payloads.removed,
2351
+ logs: logs.removed,
2352
+ config: config.removed,
2353
+ freshConfig: structuredClone(DEFAULT_CONFIG)
2354
+ };
2355
+ }
2356
+ var TARGETS = {
2357
+ payloads: { label: "Saved payloads", description: "the build history in the payloads directory", run: (cfg) => clearPayloads() },
2358
+ logs: { label: "Activity log", description: "the local run log", run: (cfg) => clearLogs() },
2359
+ shopCache: { label: "Cached store record", description: "the store name/currency snapshot \u2014 credentials kept", run: (cfg) => clearShopCache(cfg) },
2360
+ disconnect: { label: "Disconnect Shopify", description: "forget the store domain and access token", run: (cfg) => disconnectShopify(cfg) },
2361
+ all: { label: "Everything", description: "payloads, log and config \u2014 back to first run", run: () => clearAll() }
2362
+ };
2363
+
2364
+ // src/ui/screens/Clear.jsx
2365
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2366
+ function Clear({ config, onDone }) {
2367
+ const [state] = useState7(() => inspect());
2368
+ const [choice, setChoice] = useState7(null);
2369
+ const [result, setResult] = useState7(null);
2370
+ useInput9((input, key) => {
2371
+ if (key.escape) onDone();
2372
+ if (result && (key.return || input === "q")) onDone(result.freshConfig ?? config);
2373
+ });
2374
+ const run2 = () => {
2375
+ const outcome = TARGETS[choice].run(config);
2376
+ setResult(outcome);
2377
+ };
2378
+ 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` })
2385
+ ] }),
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" }) })
2388
+ ] });
2389
+ }
2390
+ if (choice) {
2391
+ 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(
2396
+ KeyValue,
2397
+ {
2398
+ keyWidth: 10,
2399
+ rows: [
2400
+ ["payloads", state.payloads.label],
2401
+ ["log", state.logs.label],
2402
+ ["config", state.config.label]
2403
+ ]
2404
+ }
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) })
2410
+ ] })
2411
+ ] });
2412
+ }
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}` })
2419
+ ] }),
2420
+ /* @__PURE__ */ jsx13(
2421
+ Select8,
2422
+ {
2423
+ visibleOptionCount: 8,
2424
+ options: Object.entries(TARGETS).map(([key, t]) => ({
2425
+ label: `${t.label.padEnd(24)} ${t.description}`,
2426
+ value: key
2427
+ })),
2428
+ onChange: setChoice
2429
+ }
2430
+ )
2431
+ ] });
2432
+ }
2433
+
2434
+ // src/ui/App.jsx
2435
+ import { jsx as jsx14 } from "react/jsx-runtime";
2436
+ function App({ config: initialConfig, initialScreen = "home" }) {
2437
+ const { exit } = useApp2();
2438
+ const [config, setConfig] = useState8(initialConfig);
2439
+ const [screen, setScreen] = useState8(initialScreen);
2440
+ const [provider, setProvider] = useState8("cashfree-occ");
2441
+ const [section, setSection] = useState8(null);
2442
+ const go = (next) => (updated) => {
2443
+ if (updated) setConfig(updated);
2444
+ setSection(null);
2445
+ setScreen(next);
2446
+ };
2447
+ const toHome = go("home");
2448
+ const toSetup = go("setup");
2449
+ const toIntegrations = go("integrations");
2450
+ switch (screen) {
2451
+ case "setup":
2452
+ return /* @__PURE__ */ jsx14(
2453
+ Setup,
2454
+ {
2455
+ config,
2456
+ onBack: toHome,
2457
+ onPick: (what) => {
2458
+ if (what === "customer") {
2459
+ setSection("Customer");
2460
+ setScreen("configure");
2461
+ } else if (what === "defaults") {
2462
+ setSection("Defaults");
2463
+ setScreen("configure");
2464
+ } else setScreen(what);
2465
+ }
2466
+ }
2467
+ );
2468
+ case "integrations":
2469
+ return /* @__PURE__ */ jsx14(
2470
+ Integrations,
2471
+ {
2472
+ config,
2473
+ onBack: toHome,
2474
+ onSetup: () => setScreen("setup"),
2475
+ onPick: (id) => {
2476
+ setProvider(id);
2477
+ setScreen("build");
2478
+ }
2479
+ }
2480
+ );
2481
+ case "connect":
2482
+ return /* @__PURE__ */ jsx14(ConnectShopify, { config, onDone: toSetup });
2483
+ case "webhook":
2484
+ return /* @__PURE__ */ jsx14(Webhook, { config, onDone: toSetup });
2485
+ case "configure":
2486
+ return /* @__PURE__ */ jsx14(Configure, { config, section, onDone: toSetup });
2487
+ case "build":
2488
+ return /* @__PURE__ */ jsx14(Build, { config, providerId: provider, onDone: toIntegrations });
2489
+ case "history":
2490
+ return /* @__PURE__ */ jsx14(History, { config, onDone: toHome });
2491
+ case "doctor":
2492
+ return /* @__PURE__ */ jsx14(Doctor, { config, onDone: toHome });
2493
+ case "clear":
2494
+ return /* @__PURE__ */ jsx14(Clear, { config, onDone: toHome });
2495
+ default:
2496
+ return /* @__PURE__ */ jsx14(Home, { config, onPick: setScreen, onQuit: exit });
2497
+ }
2498
+ }
2499
+
2500
+ // src/pretty.mjs
2501
+ var ESC = "\x1B";
2502
+ var isTTY = process.stdout.isTTY;
2503
+ var wrap = (open, close) => (s) => process.stdout.isTTY ? `${ESC}[${open}m${s}${ESC}[${close}m` : String(s);
2504
+ var c = {
2505
+ reset: (s) => s,
2506
+ bold: wrap(1, 22),
2507
+ dim: wrap(2, 22),
2508
+ italic: wrap(3, 23),
2509
+ underline: wrap(4, 24),
2510
+ red: wrap(31, 39),
2511
+ green: wrap(32, 39),
2512
+ yellow: wrap(33, 39),
2513
+ blue: wrap(34, 39),
2514
+ magenta: wrap(35, 39),
2515
+ cyan: wrap(36, 39),
2516
+ grey: wrap(90, 39),
2517
+ bgCyan: wrap(46, 49),
2518
+ bgRed: wrap(41, 49)
2519
+ };
2520
+ var sym = {
2521
+ tick: "\u2714",
2522
+ cross: "\u2716",
2523
+ warn: "\u25B2",
2524
+ info: "\u2139",
2525
+ arrow: "\u203A",
2526
+ bullet: "\u2022",
2527
+ pointer: "\u276F",
2528
+ radioOn: "\u25C9",
2529
+ radioOff: "\u25EF",
2530
+ boxOn: "\u25FC",
2531
+ boxOff: "\u25FB"
2532
+ };
2533
+ var plain = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "");
2534
+ var width = (s) => plain(s).length;
2535
+ function box(title, lines, colour = c.grey) {
2536
+ const body = Array.isArray(lines) ? lines : [lines];
2537
+ const w = Math.max(width(title) + 4, ...body.map((l) => width(l) + 2), 40);
2538
+ const pad = (s) => s + " ".repeat(Math.max(0, w - width(s)));
2539
+ console.log(colour("\u250C\u2500 ") + c.bold(title) + colour(" " + "\u2500".repeat(Math.max(0, w - width(title) - 3)) + "\u2510"));
2540
+ for (const l of body) console.log(colour("\u2502") + pad(" " + l) + colour("\u2502"));
2541
+ console.log(colour("\u2514" + "\u2500".repeat(w) + "\u2518"));
2542
+ }
2543
+ var log = {
2544
+ ok: (m) => console.log(`${c.green(sym.tick)} ${m}`),
2545
+ fail: (m) => console.log(`${c.red(sym.cross)} ${m}`),
2546
+ warn: (m) => console.log(`${c.yellow(sym.warn)} ${m}`),
2547
+ info: (m) => console.log(`${c.blue(sym.info)} ${m}`),
2548
+ step: (m) => console.log(`${c.cyan(sym.arrow)} ${m}`),
2549
+ dim: (m) => console.log(c.grey(` ${m}`)),
2550
+ blank: () => console.log()
2551
+ };
2552
+ function table(rows, headers) {
2553
+ if (!rows.length) return;
2554
+ const cols = headers || Object.keys(rows[0]);
2555
+ const widths = cols.map((col) => Math.max(width(col), ...rows.map((r) => width(String(r[col] ?? "")))));
2556
+ const line = (cells, colour = (s) => s) => " " + cells.map((cell, i) => colour(String(cell) + " ".repeat(Math.max(0, widths[i] - width(String(cell)))))).join(" ");
2557
+ console.log(line(cols, c.bold));
2558
+ console.log(" " + c.grey(widths.map((w) => "\u2500".repeat(w)).join(" ")));
2559
+ for (const r of rows) console.log(line(cols.map((col) => r[col] ?? "")));
2560
+ }
2561
+
2562
+ // src/headless/run.mjs
2563
+ async function headless(command, args, config) {
2564
+ switch (command) {
2565
+ case "build":
2566
+ return cmdBuild(args, config);
2567
+ case "send":
2568
+ return cmdSend(args, config);
2569
+ case "doctor":
2570
+ return cmdDoctor(args, config);
2571
+ case "history":
2572
+ return cmdHistory(args);
2573
+ case "clear":
2574
+ return cmdClear(args, config);
2575
+ case "configure":
2576
+ log.fail("configure needs an interactive terminal");
2577
+ log.dim(`edit ${config._path} directly, or run hookwright configure from a TTY`);
2578
+ return 1;
2579
+ default:
2580
+ log.fail(`unknown command "${command}"`);
2581
+ log.dim("try: build \xB7 send \xB7 doctor \xB7 history \xB7 configure \xB7 help");
2582
+ return 1;
2583
+ }
2584
+ }
2585
+ function requireConfig(config) {
2586
+ if (isConfigured(config)) return null;
2587
+ const missing = configStatus(config).filter((r) => !r.ok).map((r) => r.key);
2588
+ log.fail(`configuration incomplete: ${missing.join(", ")}`);
2589
+ log.dim(`run \`hookwright configure\` or edit ${config._path}`);
2590
+ return 1;
2591
+ }
2592
+ async function cmdBuild(args, config) {
2593
+ const bad = requireConfig(config);
2594
+ if (bad) return bad;
2595
+ const count = Number.parseInt(args.flags.items ?? "1", 10) || 1;
2596
+ const discount = Number.parseFloat(args.flags.discount ?? "0") || 0;
2597
+ const search = typeof args.flags.search === "string" ? args.flags.search : void 0;
2598
+ const checkImageUrls = args.flags.image !== false && args.flags["image-check"] !== false;
2599
+ log.step(`fetching products from ${config.shopify.domain}\u2026`);
2600
+ const products = await fetchCatalogue(config, { search });
2601
+ if (!products.length) {
2602
+ log.fail("Shopify returned no products");
2603
+ return 1;
2604
+ }
2605
+ const items = autoSelect(products, count);
2606
+ log.ok(`${items.length} product(s): ${items.map((i) => i.product.title).join(", ")}`);
2607
+ const built = await buildPayload2({
2608
+ cfg: config,
2609
+ items,
2610
+ discount,
2611
+ phone: typeof args.flags.phone === "string" ? args.flags.phone : void 0,
2612
+ checkImageUrls
2613
+ });
2614
+ if (args.flags.json) {
2615
+ console.log(JSON.stringify({ file: built.file, valid: built.report.ok, checks: built.report.checks, payload: built.payload }, null, 2));
2616
+ } else {
2617
+ printReport(built);
2618
+ if (args.flags.payload) {
2619
+ log.blank();
2620
+ log.step(c.bold("Payload"));
2621
+ console.log(JSON.stringify(built.payload, null, 2));
2622
+ }
2623
+ }
2624
+ 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}`);
2627
+ else log.dim(`clipboard unavailable \u2014 install xclip, xsel or wl-copy to enable copying`);
2628
+ }
2629
+ if (!built.report.ok && !args.flags.force) {
2630
+ log.fail("pre-flight failed \u2014 not sending");
2631
+ return 2;
2632
+ }
2633
+ if (args.flags.send || args.flags["dry-run"]) {
2634
+ return cmdSend({ ...args, flags: { ...args.flags, _record: built } }, config, built);
2635
+ }
2636
+ log.blank();
2637
+ log.dim(`send it with: hookwright send${args.flags["dry-run"] ? " --dry-run" : ""}`);
2638
+ return 0;
2639
+ }
2640
+ async function cmdSend(args, config, built) {
2641
+ const bad = requireConfig(config);
2642
+ if (bad) return bad;
2643
+ let record = built ? { provider: built.provider.id, payload: built.payload, body: built.body } : null;
2644
+ if (!record && typeof args.flags.file === "string") record = loadPayload(args.flags.file);
2645
+ if (!record) record = loadLast();
2646
+ if (!record) {
2647
+ log.fail("nothing to send \u2014 build a payload first");
2648
+ return 1;
2649
+ }
2650
+ const result = await dispatch({
2651
+ cfg: config,
2652
+ record,
2653
+ force: Boolean(args.flags.force),
2654
+ dryRun: Boolean(args.flags["dry-run"])
2655
+ });
2656
+ if (result.dryRun) {
2657
+ log.blank();
2658
+ log.info("dry run \u2014 not sent");
2659
+ box("Request", [
2660
+ `POST ${result.request.url}`,
2661
+ `x-webhook-timestamp ${result.request.timestamp}`,
2662
+ `x-webhook-signature ${result.request.signature}`,
2663
+ `body ${record.body.length} bytes`
2664
+ ], c.cyan);
2665
+ log.blank();
2666
+ console.log(c.grey(result.curl));
2667
+ return 0;
2668
+ }
2669
+ log.blank();
2670
+ if (result.ok) log.ok(`HTTP ${result.status} in ${result.durationMs}ms \u2014 ${explainStatus(result.status)}`);
2671
+ else log.fail(result.error ? `request failed: ${result.error}` : `HTTP ${result.status} \u2014 ${explainStatus(result.status)}`);
2672
+ if (result.body) log.dim(`response: ${String(result.body).slice(0, 300)}`);
2673
+ log.blank();
2674
+ console.log(c.grey(result.curl));
2675
+ return result.ok ? 0 : 3;
2676
+ }
2677
+ async function cmdDoctor(args, config) {
2678
+ const result = await run(config);
2679
+ const groups = [
2680
+ ["Configuration", result.config],
2681
+ ["Connectivity", result.connectivity],
2682
+ ["Runtime", result.runtime]
2683
+ ];
2684
+ for (const [title, checks] of groups) {
2685
+ log.blank();
2686
+ log.step(c.bold(title));
2687
+ for (const check of checks) {
2688
+ const mark = check.ok ? c.green(sym.tick) : c.red(sym.cross);
2689
+ console.log(` ${mark} ${c.bold(String(check.name).padEnd(20))} ${c.grey(check.detail)}`);
2690
+ }
2691
+ }
2692
+ log.blank();
2693
+ const failed = [...result.config, ...result.connectivity, ...result.runtime].filter((c2) => !c2.ok);
2694
+ if (args.flags.json) console.log(JSON.stringify(result, null, 2));
2695
+ return failed.length ? 1 : 0;
2696
+ }
2697
+ function cmdHistory(args) {
2698
+ const items = listPayloads(Number.parseInt(args.flags.limit ?? "20", 10) || 20);
2699
+ if (args.flags.json) {
2700
+ console.log(JSON.stringify(items, null, 2));
2701
+ return 0;
2702
+ }
2703
+ if (!items.length) {
2704
+ log.dim("no payloads built yet");
2705
+ return 0;
2706
+ }
2707
+ table(
2708
+ items.map((i) => ({
2709
+ when: i.savedAt.replace("T", " ").slice(0, 19),
2710
+ provider: i.provider,
2711
+ env: i.meta.environment ?? "\u2014",
2712
+ total: i.meta.totalPrice != null ? `${i.meta.totalPrice} ${i.meta.currency ?? ""}` : "\u2014",
2713
+ valid: i.meta.valid ? c.green(sym.tick) : c.red(sym.cross),
2714
+ file: i.file
2715
+ })),
2716
+ ["when", "provider", "env", "total", "valid", "file"]
2717
+ );
2718
+ return 0;
2719
+ }
2720
+ function cmdClear(args, config) {
2721
+ const state = inspect();
2722
+ const target = args.flags.all ? "all" : args.flags.payloads ? "payloads" : args.flags.logs ? "logs" : args.flags.disconnect ? "disconnect" : args.flags["shop-cache"] ? "shopCache" : null;
2723
+ if (!target) {
2724
+ log.step("locally cached data");
2725
+ log.dim(state.payloads.label);
2726
+ log.dim(state.logs.label);
2727
+ log.dim(state.config.label);
2728
+ log.blank();
2729
+ log.info("choose what to remove:");
2730
+ log.dim("--payloads saved build history");
2731
+ log.dim("--logs activity log");
2732
+ log.dim("--shop-cache cached store record (credentials kept)");
2733
+ log.dim("--disconnect forget the Shopify domain and token");
2734
+ log.dim("--all payloads, log and config \u2014 back to first run");
2735
+ log.dim("add --yes to skip the confirmation");
2736
+ return 0;
2737
+ }
2738
+ if (!args.flags.yes) {
2739
+ log.warn(`this permanently deletes ${TARGETS[target].description}`);
2740
+ log.dim("re-run with --yes to confirm");
2741
+ return 1;
2742
+ }
2743
+ const result = TARGETS[target].run(config);
2744
+ if (target === "all") {
2745
+ log.ok(`removed ${result.payloads} payload(s), ${result.logs} log, ${result.config} config file \u2014 back to first-run state`);
2746
+ } else {
2747
+ log.ok(`${TARGETS[target].label}: ${result.removed} item(s) removed`);
2748
+ }
2749
+ return 0;
2750
+ }
2751
+ function printReport(built) {
2752
+ const d = built.payload.data;
2753
+ log.blank();
2754
+ 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})`,
2760
+ `saved ${built.file}`
2761
+ ], c.green);
2762
+ log.blank();
2763
+ log.step(c.bold("Pre-flight"));
2764
+ for (const check of built.report.checks) {
2765
+ const mark = check.ok ? c.green(sym.tick) : check.warn ? c.yellow(sym.warn) : c.red(sym.cross);
2766
+ console.log(` ${mark} ${c.bold(check.name.padEnd(24))} ${c.grey(check.detail)}`);
2767
+ }
2768
+ log.blank();
2769
+ log.step(c.bold("Field coverage"));
2770
+ table(
2771
+ built.report.coverage.map((c2) => ({
2772
+ field: c2.field,
2773
+ "maps to": c2.maps_to,
2774
+ value: c2.status === "MISSING" ? c.red(c2.value) : c2.value
2775
+ })),
2776
+ ["field", "maps to", "value"]
2777
+ );
2778
+ }
2779
+
2780
+ // src/cli.jsx
2781
+ import { jsx as jsx15 } from "react/jsx-runtime";
2782
+ var HELP = `
2783
+ ${c.bold("hookwright")} \u2014 generate real, signed provider webhooks from a live Shopify catalogue
2784
+
2785
+ ${c.bold("USAGE")}
2786
+ hookwright launch the interactive UI
2787
+ hookwright <command> [options] run headless (CI / scripting)
2788
+
2789
+ ${c.bold("COMMANDS")}
2790
+ build build a payload from real Shopify products
2791
+ send sign and POST the last built payload
2792
+ doctor verify credentials and connectivity
2793
+ history list saved payloads
2794
+ clear remove locally cached data (payloads, log, config)
2795
+ configure launch the setup wizard
2796
+ help show this message
2797
+
2798
+ ${c.bold("BUILD OPTIONS")}
2799
+ --items <n> how many products to put in the cart (default 1)
2800
+ --search <text> only consider products matching a title
2801
+ --discount <n> cart discount in major currency units (default 0)
2802
+ --phone <e164> override the configured test phone
2803
+ --send transmit immediately after building
2804
+ --dry-run print the request and curl, do not send
2805
+ --no-image-check skip HEAD-checking product image URLs
2806
+ --payload print the full generated payload
2807
+ --no-copy do not copy the payload to the clipboard
2808
+ --json machine-readable output
2809
+
2810
+ ${c.bold("SEND OPTIONS")}
2811
+ --file <path> send a specific saved payload
2812
+ --dry-run print the request without sending
2813
+ --force send even if pre-flight fails or target looks like prod
2814
+
2815
+ ${c.bold("CLEAR OPTIONS")}
2816
+ --payloads delete saved payloads
2817
+ --logs delete the activity log
2818
+ --shop-cache drop the cached store record, keep credentials
2819
+ --disconnect forget the Shopify domain and token
2820
+ --all payloads + log + config, back to first run
2821
+ --yes skip the confirmation
2822
+
2823
+ ${c.bold("GLOBAL")}
2824
+ --config <path> use a different config file
2825
+ --verbose verbose logging
2826
+ --version print version
2827
+
2828
+ ${c.grey(`config: ${CONFIG_PATH}`)}
2829
+ `;
2830
+ function parseArgs(argv) {
2831
+ const args = { _: [], flags: {} };
2832
+ for (let i = 0; i < argv.length; i += 1) {
2833
+ const token = argv[i];
2834
+ if (token.startsWith("--")) {
2835
+ const key = token.slice(2);
2836
+ const next = argv[i + 1];
2837
+ if (key.startsWith("no-")) {
2838
+ args.flags[key.slice(3)] = false;
2839
+ } else if (next && !next.startsWith("--")) {
2840
+ args.flags[key] = next;
2841
+ i += 1;
2842
+ } else {
2843
+ args.flags[key] = true;
2844
+ }
2845
+ } else {
2846
+ args._.push(token);
2847
+ }
2848
+ }
2849
+ return args;
2850
+ }
2851
+ async function main() {
2852
+ const args = parseArgs(process.argv.slice(2));
2853
+ const command = args._[0];
2854
+ if (args.flags.verbose) setVerbose(true);
2855
+ if (args.flags.version) {
2856
+ const { version } = await Promise.resolve().then(() => (init_package(), package_exports)).then((m) => m.default).catch(() => ({ version: "dev" }));
2857
+ console.log(version);
2858
+ return;
2859
+ }
2860
+ if (command === "help" || args.flags.help) {
2861
+ console.log(HELP);
2862
+ return;
2863
+ }
2864
+ const config = loadConfig(args.flags.config);
2865
+ if (command && command !== "ui") {
2866
+ if (command === "configure" && process.stdin.isTTY) {
2867
+ render(/* @__PURE__ */ jsx15(App, { config, initialScreen: "connect" }));
2868
+ return;
2869
+ }
2870
+ const code = await headless(command, args, config);
2871
+ process.exitCode = code;
2872
+ return;
2873
+ }
2874
+ if (!process.stdin.isTTY) {
2875
+ console.log(HELP);
2876
+ console.error(c.yellow("\nno TTY detected \u2014 use a headless command (build / send / doctor / history)"));
2877
+ process.exitCode = 1;
2878
+ return;
2879
+ }
2880
+ render(/* @__PURE__ */ jsx15(App, { config, initialScreen: "home" }));
2881
+ }
2882
+ main().catch((err) => {
2883
+ console.error(c.red(`
2884
+ ${err.message}`));
2885
+ if (err.hint) console.error(c.yellow(` ${err.hint}`));
2886
+ process.exitCode = 1;
2887
+ });
2888
+ export {
2889
+ parseArgs
2890
+ };