@binaryandthread/cli 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/bin.js +323 -100
- package/dist/bin.js.map +1 -1
- package/dist/{browser-EO5EQUXQ.js → browser-WCNC4AWS.js} +22 -3
- package/dist/browser-WCNC4AWS.js.map +1 -0
- package/dist/{chunk-B6PV7EQG.js → chunk-NJ3DW5AX.js} +113 -61
- package/dist/chunk-NJ3DW5AX.js.map +1 -0
- package/dist/{interactive-BYDQJSG4.js → interactive-5PBJCENZ.js} +2 -2
- package/package.json +1 -1
- package/dist/browser-EO5EQUXQ.js.map +0 -1
- package/dist/chunk-B6PV7EQG.js.map +0 -1
- /package/dist/{interactive-BYDQJSG4.js.map → interactive-5PBJCENZ.js.map} +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
import "./chunk-7OCVIDC7.js";
|
|
3
3
|
|
|
4
4
|
// src/browser.ts
|
|
5
|
+
var DEFAULT_CHECKOUT_HOSTS = ["binaryandthread.com"];
|
|
6
|
+
function isSafeCheckoutUrl(url, allowedHosts = DEFAULT_CHECKOUT_HOSTS) {
|
|
7
|
+
try {
|
|
8
|
+
const u = new URL(url);
|
|
9
|
+
if (u.protocol !== "https:") return false;
|
|
10
|
+
const host = u.hostname.toLowerCase();
|
|
11
|
+
return allowedHosts.some((a) => host === a || host.endsWith("." + a));
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
5
16
|
async function defaultOpener(url) {
|
|
6
17
|
const mod = await import("open");
|
|
7
18
|
return mod.default(url);
|
|
@@ -24,15 +35,22 @@ function renderQr(url, qr) {
|
|
|
24
35
|
async function openCheckout(url, opts = {}) {
|
|
25
36
|
const opener = opts.opener ?? defaultOpener;
|
|
26
37
|
const qr = opts.qr ?? await defaultQr();
|
|
27
|
-
const
|
|
38
|
+
const safe = isSafeCheckoutUrl(url, opts.allowedHosts);
|
|
39
|
+
const lines = [];
|
|
28
40
|
let opened = false;
|
|
41
|
+
if (!safe) {
|
|
42
|
+
lines.push("\u26A0 this checkout link isn't the expected binaryandthread.com https page.");
|
|
43
|
+
lines.push(" not opening it automatically \u2014 verify the domain, then open the link below yourself.");
|
|
44
|
+
lines.push(` link \u2192 ${url}`);
|
|
45
|
+
return { opened: false, lines };
|
|
46
|
+
}
|
|
47
|
+
const qrString = renderQr(url, qr);
|
|
29
48
|
try {
|
|
30
49
|
await opener(url);
|
|
31
50
|
opened = true;
|
|
32
51
|
} catch {
|
|
33
52
|
opened = false;
|
|
34
53
|
}
|
|
35
|
-
const lines = [];
|
|
36
54
|
lines.push("\u2699 opening checkout in your browser. cards never touch the terminal.");
|
|
37
55
|
if (qrString) {
|
|
38
56
|
lines.push(qrString);
|
|
@@ -45,6 +63,7 @@ async function openCheckout(url, opts = {}) {
|
|
|
45
63
|
return { opened, lines };
|
|
46
64
|
}
|
|
47
65
|
export {
|
|
66
|
+
isSafeCheckoutUrl,
|
|
48
67
|
openCheckout
|
|
49
68
|
};
|
|
50
|
-
//# sourceMappingURL=browser-
|
|
69
|
+
//# sourceMappingURL=browser-WCNC4AWS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/browser.ts"],"sourcesContent":["// The browser/QR handoff — the Spike-A floor's client side (F-C5).\n//\n// The terminal NEVER takes a card. After minting the checkout, we hand the buyer off to the\n// storefront checkout page (Stripe Elements lives there, not here). We do that two ways at\n// once so the buyer always has a path:\n// 1. open the default browser at the checkout url, AND\n// 2. render a scannable QR of the same url (so a phone can complete the checkout).\n//\n// This MUST degrade gracefully: a headless/SSH/CI session has no browser to open, so a\n// failed open is normal, not an error. On failure we still print the url + the QR and a\n// one-line \"open this manually\" hint, and we NEVER throw — a buy is never blocked on the\n// browser opening cleanly (F-C5). The `open` and `qrcode-terminal` libraries are injected so\n// the handoff is testable without spawning a real browser or a real QR renderer.\n\n/** The hosts a checkout_url is allowed to point at before we'll auto-open it. The brand storefront\n * (checkout_url = STOREFRONT_URL + path, and STOREFRONT_URL is binaryandthread.com in prod). */\nconst DEFAULT_CHECKOUT_HOSTS = [\"binaryandthread.com\"]\n\n/**\n * Is `url` safe to auto-open? Only https on an allowed host (exact or a subdomain). The checkout_url\n * comes from the backend; this is the last gate before an OS-level open() — it refuses http/file/\n * custom-scheme and a swapped host, so a coerced URL can't be launched or QR'd (defense-in-depth).\n */\nexport function isSafeCheckoutUrl(url: string, allowedHosts: string[] = DEFAULT_CHECKOUT_HOSTS): boolean {\n try {\n const u = new URL(url)\n if (u.protocol !== \"https:\") return false\n const host = u.hostname.toLowerCase()\n return allowedHosts.some((a) => host === a || host.endsWith(\".\" + a))\n } catch {\n return false\n }\n}\n\n/** open the url in the default browser; returns when the handoff is launched. */\nexport type Opener = (url: string) => Promise<unknown>\n\n/** qrcode-terminal-shaped renderer: generate(url, opts, cb) → cb(qrString). */\nexport type QrRenderer = (\n url: string,\n opts: { small?: boolean },\n cb: (qr: string) => void\n) => void\n\nexport interface OpenCheckoutOptions {\n /** injectable browser opener (default: the `open` npm package). */\n opener?: Opener\n /** injectable QR renderer (default: the `qrcode-terminal` package). */\n qr?: QrRenderer\n /** hosts a checkout_url may point at to be auto-opened (default: the brand storefront). */\n allowedHosts?: string[]\n}\n\nexport interface OpenCheckoutResult {\n /** whether the default browser was launched (false on a headless/CI session). */\n opened: boolean\n /** voice-compliant output lines (QR + url floor + a manual-open hint when needed). */\n lines: string[]\n}\n\n/** Lazily load the real `open` package (ESM, spawns OS binaries — kept external by tsup). */\nasync function defaultOpener(url: string): Promise<unknown> {\n const mod = (await import(\"open\")) as unknown as { default: Opener }\n return mod.default(url)\n}\n\n/** Lazily load the real `qrcode-terminal` package. */\nasync function defaultQr(): Promise<QrRenderer> {\n const mod = (await import(\"qrcode-terminal\")) as unknown as {\n default: { generate: QrRenderer }\n }\n return mod.default.generate\n}\n\n/** Render the QR string for `url`, never throwing (returns \"\" if the renderer blows up). */\nfunction renderQr(url: string, qr: QrRenderer): string {\n try {\n let out = \"\"\n qr(url, { small: true }, (s) => {\n out = s\n })\n return out\n } catch {\n return \"\"\n }\n}\n\n/**\n * Open the storefront checkout in the browser AND print a scannable QR. Degrades gracefully\n * (never throws): on a failed browser open it still emits the QR + the url + a manual hint.\n * The caller prints `result.lines`; cards never touch the terminal — they live on that page.\n */\nexport async function openCheckout(\n url: string,\n opts: OpenCheckoutOptions = {}\n): Promise<OpenCheckoutResult> {\n const opener = opts.opener ?? defaultOpener\n const qr = opts.qr ?? (await defaultQr())\n const safe = isSafeCheckoutUrl(url, opts.allowedHosts)\n\n const lines: string[] = []\n let opened = false\n\n if (!safe) {\n // The URL isn't the expected brand https page → NEVER auto-open or QR it (a coerced/misconfigured\n // checkout_url could be a phishing origin or a non-http scheme). Show it with a warning so the\n // buyer can inspect + decide; the buy is not blocked (the link floor is still printed).\n lines.push(\"⚠ this checkout link isn't the expected binaryandthread.com https page.\")\n lines.push(\" not opening it automatically — verify the domain, then open the link below yourself.\")\n lines.push(` link → ${url}`)\n return { opened: false, lines }\n }\n\n // Safe: render the QR (part of the floor) + try to open the browser.\n const qrString = renderQr(url, qr)\n try {\n await opener(url)\n opened = true\n } catch {\n // headless / SSH / CI — no browser to open. Not an error; degrade to the manual floor.\n opened = false\n }\n\n lines.push(\"⚙ opening checkout in your browser. cards never touch the terminal.\")\n if (qrString) {\n lines.push(qrString)\n lines.push(\" scan the code above to finish on your phone.\")\n }\n if (!opened) {\n lines.push(\" couldn't open a browser here — open this link manually:\")\n }\n // the url is the floor — always printed last so it's the easy copy target.\n lines.push(` link → ${url}`)\n\n return { opened, lines }\n}\n"],"mappings":";;;;AAgBA,IAAM,yBAAyB,CAAC,qBAAqB;AAO9C,SAAS,kBAAkB,KAAa,eAAyB,wBAAiC;AACvG,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,QAAI,EAAE,aAAa,SAAU,QAAO;AACpC,UAAM,OAAO,EAAE,SAAS,YAAY;AACpC,WAAO,aAAa,KAAK,CAAC,MAAM,SAAS,KAAK,KAAK,SAAS,MAAM,CAAC,CAAC;AAAA,EACtE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AA6BA,eAAe,cAAc,KAA+B;AAC1D,QAAM,MAAO,MAAM,OAAO,MAAM;AAChC,SAAO,IAAI,QAAQ,GAAG;AACxB;AAGA,eAAe,YAAiC;AAC9C,QAAM,MAAO,MAAM,OAAO,iBAAiB;AAG3C,SAAO,IAAI,QAAQ;AACrB;AAGA,SAAS,SAAS,KAAa,IAAwB;AACrD,MAAI;AACF,QAAI,MAAM;AACV,OAAG,KAAK,EAAE,OAAO,KAAK,GAAG,CAAC,MAAM;AAC9B,YAAM;AAAA,IACR,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,aACpB,KACA,OAA4B,CAAC,GACA;AAC7B,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,KAAK,KAAK,MAAO,MAAM,UAAU;AACvC,QAAM,OAAO,kBAAkB,KAAK,KAAK,YAAY;AAErD,QAAM,QAAkB,CAAC;AACzB,MAAI,SAAS;AAEb,MAAI,CAAC,MAAM;AAIT,UAAM,KAAK,+EAA0E;AACrF,UAAM,KAAK,8FAAyF;AACpG,UAAM,KAAK,kBAAa,GAAG,EAAE;AAC7B,WAAO,EAAE,QAAQ,OAAO,MAAM;AAAA,EAChC;AAGA,QAAM,WAAW,SAAS,KAAK,EAAE;AACjC,MAAI;AACF,UAAM,OAAO,GAAG;AAChB,aAAS;AAAA,EACX,QAAQ;AAEN,aAAS;AAAA,EACX;AAEA,QAAM,KAAK,2EAAsE;AACjF,MAAI,UAAU;AACZ,UAAM,KAAK,QAAQ;AACnB,UAAM,KAAK,iDAAiD;AAAA,EAC9D;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,KAAK,iEAA4D;AAAA,EACzE;AAEA,QAAM,KAAK,kBAAa,GAAG,EAAE;AAE7B,SAAO,EAAE,QAAQ,MAAM;AACzB;","names":[]}
|
|
@@ -286,24 +286,45 @@ var StoreClient = class {
|
|
|
286
286
|
return this.regionsCache;
|
|
287
287
|
}
|
|
288
288
|
/**
|
|
289
|
-
* Resolve a region by country code,
|
|
290
|
-
*
|
|
289
|
+
* Resolve a region by country code, WITH provenance (matched / home / requested), so callers on
|
|
290
|
+
* the money path can fail closed on an unmatched explicit country instead of silently pricing in
|
|
291
|
+
* the wrong currency (F-A2). Region #1 is the launch/home default — SEK today, derived, never a
|
|
292
|
+
* hardcoded literal.
|
|
293
|
+
*
|
|
294
|
+
* Semantics:
|
|
295
|
+
* - country given AND a region ships there → { matched: true, home: false }.
|
|
296
|
+
* - country given, none ships there → { matched: false, home: false } (money surfaces MUST
|
|
297
|
+
* refuse; discovery may show the home region + a warning). We still return a region so a
|
|
298
|
+
* discovery command can render *something*, but `matched:false` is the signal to fail closed.
|
|
299
|
+
* - no country given → { matched: false, home: true } (the legitimate
|
|
300
|
+
* launch default — not a "wrong" currency).
|
|
291
301
|
*/
|
|
292
|
-
async
|
|
302
|
+
async resolveRegionResult(countryCode) {
|
|
293
303
|
const regions = this.regionsCache ?? await this.getRegions();
|
|
294
304
|
if (regions.length === 0) {
|
|
295
305
|
throw new Error("no regions configured \u2014 cannot resolve a region for pricing");
|
|
296
306
|
}
|
|
307
|
+
const home = regions[0];
|
|
297
308
|
if (countryCode) {
|
|
298
309
|
const cc = countryCode.toLowerCase();
|
|
299
310
|
const match = regions.find(
|
|
300
311
|
(r) => (r.countries ?? []).some((c) => (c.iso_2 ?? "").toLowerCase() === cc)
|
|
301
312
|
);
|
|
302
313
|
if (match) {
|
|
303
|
-
return match;
|
|
314
|
+
return { region: match, matched: true, requested: cc, home: false };
|
|
304
315
|
}
|
|
316
|
+
this.logWarn("store.region.unmatched", { countryCode: cc, fallback: home.id });
|
|
317
|
+
return { region: home, matched: false, requested: cc, home: false };
|
|
305
318
|
}
|
|
306
|
-
return
|
|
319
|
+
return { region: home, matched: false, requested: void 0, home: true };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Resolve a region by country code, falling back to region #1. A thin wrapper over
|
|
323
|
+
* resolveRegionResult for callers that only need the region (back-compat).
|
|
324
|
+
* SEK is region #1 by configuration — NEVER a hardcoded literal.
|
|
325
|
+
*/
|
|
326
|
+
async resolveRegion(countryCode) {
|
|
327
|
+
return (await this.resolveRegionResult(countryCode)).region;
|
|
307
328
|
}
|
|
308
329
|
// ----- products (region resolved BEFORE pricing) ---------------------------
|
|
309
330
|
async listProducts(opts) {
|
|
@@ -657,6 +678,9 @@ function productLore(p) {
|
|
|
657
678
|
).map((s) => ({ url: s.url, label: s.label }));
|
|
658
679
|
return { text, sources };
|
|
659
680
|
}
|
|
681
|
+
function isCartTaxInclusive(cart) {
|
|
682
|
+
return (cart?.items ?? []).some((i) => i.is_tax_inclusive === true);
|
|
683
|
+
}
|
|
660
684
|
|
|
661
685
|
// src/voice.ts
|
|
662
686
|
var GLYPH = {
|
|
@@ -805,12 +829,15 @@ var BOX = {
|
|
|
805
829
|
ml: "\u251C",
|
|
806
830
|
mr: "\u2524"
|
|
807
831
|
};
|
|
832
|
+
function cursor(style) {
|
|
833
|
+
return cyan(style, "_");
|
|
834
|
+
}
|
|
808
835
|
function header(style, title, width = 56) {
|
|
809
836
|
const label = ` ${title} `;
|
|
810
837
|
const lead = BOX.h.repeat(2);
|
|
811
838
|
const used = lead.length + label.length;
|
|
812
|
-
const tail = BOX.h.repeat(Math.max(2, width - used));
|
|
813
|
-
return `${lead}${cyan(style, label)}${tail}`;
|
|
839
|
+
const tail = BOX.h.repeat(Math.max(2, width - used - 2));
|
|
840
|
+
return `${lead}${cyan(style, label)}${tail} ${cursor(style)}`;
|
|
814
841
|
}
|
|
815
842
|
function rule(width = 56) {
|
|
816
843
|
return BOX.h.repeat(width);
|
|
@@ -859,6 +886,20 @@ function colourMarker(colours) {
|
|
|
859
886
|
}
|
|
860
887
|
|
|
861
888
|
// src/commands/shared.ts
|
|
889
|
+
function regionLine(rr, style) {
|
|
890
|
+
const cc = (rr.requested ?? rr.region.countries?.[0]?.iso_2 ?? "").toLowerCase();
|
|
891
|
+
const loc = cc ? `${cc} \xB7 ` : "";
|
|
892
|
+
const nudge = rr.home ? " \xB7 not you? \u2192 bt region" : "";
|
|
893
|
+
return dim(style, `${GLYPH.gear} region: ${loc}${rr.region.currency_code}${nudge}`);
|
|
894
|
+
}
|
|
895
|
+
var SIZE_ORDER = ["xxs", "xs", "s", "m", "l", "xl", "2xl", "xxl", "3xl", "xxxl", "4xl", "5xl"];
|
|
896
|
+
function sortSizes(sizes) {
|
|
897
|
+
const rank = (s) => {
|
|
898
|
+
const i = SIZE_ORDER.indexOf(s.trim().toLowerCase());
|
|
899
|
+
return i === -1 ? SIZE_ORDER.length : i;
|
|
900
|
+
};
|
|
901
|
+
return [...sizes].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
902
|
+
}
|
|
862
903
|
function brandOf(args) {
|
|
863
904
|
return args?.brand ?? PLAIN;
|
|
864
905
|
}
|
|
@@ -869,11 +910,15 @@ function fmtPrice(amount, currency) {
|
|
|
869
910
|
const cur = (currency ?? "").toUpperCase();
|
|
870
911
|
return cur ? `${amount} ${cur}` : `${amount}`;
|
|
871
912
|
}
|
|
913
|
+
function taxSuffix(isTaxInclusive) {
|
|
914
|
+
return isTaxInclusive ? " incl. VAT" : "";
|
|
915
|
+
}
|
|
872
916
|
function firstPrice(p) {
|
|
873
917
|
const v = (p.variants ?? [])[0];
|
|
874
918
|
return {
|
|
875
919
|
amount: v?.calculated_price?.calculated_amount ?? null,
|
|
876
|
-
currency: v?.calculated_price?.currency_code ?? null
|
|
920
|
+
currency: v?.calculated_price?.currency_code ?? null,
|
|
921
|
+
taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null
|
|
877
922
|
};
|
|
878
923
|
}
|
|
879
924
|
function stageMarker(channelStage) {
|
|
@@ -986,12 +1031,32 @@ function resolveOutput(program) {
|
|
|
986
1031
|
const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor: opts.color === false });
|
|
987
1032
|
return { json, brand };
|
|
988
1033
|
}
|
|
1034
|
+
function failResult(spec) {
|
|
1035
|
+
const numericCode = typeof spec.code === "number" && spec.code >= 1 && spec.code <= 255;
|
|
1036
|
+
const exitCode = spec.exit ?? (numericCode ? spec.code : EXIT.GENERIC);
|
|
1037
|
+
return {
|
|
1038
|
+
lines: [fail(spec)],
|
|
1039
|
+
exitCode,
|
|
1040
|
+
json: {
|
|
1041
|
+
error: {
|
|
1042
|
+
code: exitCode,
|
|
1043
|
+
message: spec.what,
|
|
1044
|
+
...spec.fix ? { fix: spec.fix } : {},
|
|
1045
|
+
...spec.link ? { link: spec.link } : {}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
989
1050
|
function printResult(res, json) {
|
|
990
1051
|
if (res.exitCode) {
|
|
991
1052
|
process.exitCode = res.exitCode;
|
|
992
1053
|
}
|
|
993
|
-
if (json
|
|
994
|
-
|
|
1054
|
+
if (json) {
|
|
1055
|
+
if (res.json !== void 0) {
|
|
1056
|
+
process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
|
|
1057
|
+
} else {
|
|
1058
|
+
process.stderr.write(res.lines.join("\n") + "\n");
|
|
1059
|
+
}
|
|
995
1060
|
return;
|
|
996
1061
|
}
|
|
997
1062
|
process.stdout.write(res.lines.join("\n") + "\n");
|
|
@@ -1066,30 +1131,21 @@ async function defaultRender(url, opts) {
|
|
|
1066
1131
|
// src/commands/view.ts
|
|
1067
1132
|
async function runView(client, config, args) {
|
|
1068
1133
|
const style = brandOf(args);
|
|
1069
|
-
const
|
|
1134
|
+
const rr = await client.resolveRegionResult(config.region);
|
|
1135
|
+
const region = rr.region;
|
|
1070
1136
|
const picked = await resolveProduct(client, region, args.handle);
|
|
1071
1137
|
const product = picked.product;
|
|
1072
1138
|
if (!product) {
|
|
1073
1139
|
if (picked.candidates && picked.candidates.length > 1) {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
...picked.candidates.map((p) => ` ${p.handle ?? p.id}`),
|
|
1078
|
-
" or be exact \u2192 bt view <slug>"
|
|
1079
|
-
],
|
|
1080
|
-
exitCode: EXIT.DATAERR
|
|
1081
|
-
};
|
|
1140
|
+
const r = failResult({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${picked.candidates.length} designs \u2014 pick one:` });
|
|
1141
|
+
r.lines.push(...picked.candidates.map((p) => ` ${p.handle ?? p.id}`), " or be exact \u2192 bt view <slug>");
|
|
1142
|
+
return r;
|
|
1082
1143
|
}
|
|
1083
|
-
return {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
fix: "bt browse (to see what's in stock)"
|
|
1089
|
-
})
|
|
1090
|
-
],
|
|
1091
|
-
exitCode: EXIT.UNAVAILABLE
|
|
1092
|
-
};
|
|
1144
|
+
return failResult({
|
|
1145
|
+
code: EXIT.UNAVAILABLE,
|
|
1146
|
+
what: `no design here called "${args.handle}".`,
|
|
1147
|
+
fix: "bt browse (to see what's in stock)"
|
|
1148
|
+
});
|
|
1093
1149
|
}
|
|
1094
1150
|
const { title, handle } = displayName(product);
|
|
1095
1151
|
const colours = productColours(product);
|
|
@@ -1110,18 +1166,19 @@ async function runView(client, config, args) {
|
|
|
1110
1166
|
if (colours.length > 0) {
|
|
1111
1167
|
lines.push(` colours ${colours.map((c) => `${colourSwatch(c)} ${c}`).join(" \xB7 ")}`);
|
|
1112
1168
|
for (const c of colours) {
|
|
1113
|
-
const sizes = sizesForColour(product, c);
|
|
1169
|
+
const sizes = sortSizes(sizesForColour(product, c));
|
|
1114
1170
|
lines.push(` ${colourSwatch(c)} ${c.padEnd(6)} ${sizes.join(" ")}`);
|
|
1115
1171
|
}
|
|
1116
1172
|
} else {
|
|
1117
|
-
const sizes = (product.variants ?? []).map((v) => v.title ?? "?");
|
|
1173
|
+
const sizes = sortSizes((product.variants ?? []).map((v) => v.title ?? "?"));
|
|
1118
1174
|
lines.push(` sizes ${sizes.join(" ")}`);
|
|
1119
1175
|
}
|
|
1120
1176
|
const v0 = (product.variants ?? [])[0];
|
|
1121
1177
|
if (v0?.calculated_price) {
|
|
1122
1178
|
lines.push(
|
|
1123
|
-
` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))}
|
|
1179
|
+
` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`
|
|
1124
1180
|
);
|
|
1181
|
+
lines.push(` ${regionLine(rr, style)}`);
|
|
1125
1182
|
}
|
|
1126
1183
|
let imageUrl = product.thumbnail ?? null;
|
|
1127
1184
|
let blackNote = null;
|
|
@@ -1185,27 +1242,24 @@ function registerView(program, makeClient, loadConfig) {
|
|
|
1185
1242
|
// src/commands/stage.ts
|
|
1186
1243
|
async function runStage(client, config, args) {
|
|
1187
1244
|
const style = brandOf(args);
|
|
1188
|
-
const
|
|
1245
|
+
const rr = await client.resolveRegionResult(config.region);
|
|
1246
|
+
if (rr.requested && !rr.matched) {
|
|
1247
|
+
return failResult({
|
|
1248
|
+
code: EXIT.DATAERR,
|
|
1249
|
+
what: `we don't ship to ${rr.requested} yet.`,
|
|
1250
|
+
money: "nothing staged, nothing charged.",
|
|
1251
|
+
fix: "bt region --list (see where we ship, then bt region <cc>)"
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
const region = rr.region;
|
|
1189
1255
|
const resolved = await resolveProduct(client, region, args.handle);
|
|
1190
1256
|
if (!resolved.product) {
|
|
1191
1257
|
if (resolved.candidates && resolved.candidates.length > 1) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`),
|
|
1196
|
-
" or be exact \u2192 bt stage <slug>"
|
|
1197
|
-
],
|
|
1198
|
-
exitCode: EXIT.DATAERR
|
|
1199
|
-
// ambiguous input → non-zero so `bt stage x && bt checkout` halts (d10)
|
|
1200
|
-
};
|
|
1258
|
+
const r = failResult({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` });
|
|
1259
|
+
r.lines.push(...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`), " or be exact \u2192 bt stage <slug>");
|
|
1260
|
+
return r;
|
|
1201
1261
|
}
|
|
1202
|
-
return {
|
|
1203
|
-
lines: [
|
|
1204
|
-
fail({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" })
|
|
1205
|
-
],
|
|
1206
|
-
exitCode: EXIT.UNAVAILABLE
|
|
1207
|
-
// not-found → non-zero (d10)
|
|
1208
|
-
};
|
|
1262
|
+
return failResult({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" });
|
|
1209
1263
|
}
|
|
1210
1264
|
const product = resolved.product;
|
|
1211
1265
|
const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
|
|
@@ -1215,17 +1269,11 @@ async function runStage(client, config, args) {
|
|
|
1215
1269
|
const variant = findVariant(product, { size, colour });
|
|
1216
1270
|
if (!variant) {
|
|
1217
1271
|
const have = colours.length ? ` available colours: ${colours.join(", ")}.` : "";
|
|
1218
|
-
return {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
|
|
1224
|
-
})
|
|
1225
|
-
],
|
|
1226
|
-
exitCode: EXIT.DATAERR
|
|
1227
|
-
// unprocessable size+colour combo → non-zero (d10)
|
|
1228
|
-
};
|
|
1272
|
+
return failResult({
|
|
1273
|
+
code: EXIT.DATAERR,
|
|
1274
|
+
what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
|
|
1275
|
+
fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
|
|
1276
|
+
});
|
|
1229
1277
|
}
|
|
1230
1278
|
if (args.dryRun) {
|
|
1231
1279
|
const colourSuffix2 = colour ? ` (${colour})` : "";
|
|
@@ -1324,6 +1372,7 @@ export {
|
|
|
1324
1372
|
sizesForColour,
|
|
1325
1373
|
findVariant,
|
|
1326
1374
|
productLore,
|
|
1375
|
+
isCartTaxInclusive,
|
|
1327
1376
|
GLYPH,
|
|
1328
1377
|
ok,
|
|
1329
1378
|
warn,
|
|
@@ -1342,8 +1391,10 @@ export {
|
|
|
1342
1391
|
contributionBar,
|
|
1343
1392
|
colourSwatch,
|
|
1344
1393
|
colourMarker,
|
|
1394
|
+
regionLine,
|
|
1345
1395
|
brandOf,
|
|
1346
1396
|
fmtPrice,
|
|
1397
|
+
taxSuffix,
|
|
1347
1398
|
firstPrice,
|
|
1348
1399
|
stageMarker,
|
|
1349
1400
|
displayName,
|
|
@@ -1352,10 +1403,11 @@ export {
|
|
|
1352
1403
|
resolveLineItemRef,
|
|
1353
1404
|
resolveProduct,
|
|
1354
1405
|
resolveOutput,
|
|
1406
|
+
failResult,
|
|
1355
1407
|
printResult,
|
|
1356
1408
|
runView,
|
|
1357
1409
|
registerView,
|
|
1358
1410
|
runStage,
|
|
1359
1411
|
registerStage
|
|
1360
1412
|
};
|
|
1361
|
-
//# sourceMappingURL=chunk-
|
|
1413
|
+
//# sourceMappingURL=chunk-NJ3DW5AX.js.map
|