@binaryandthread/cli 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -545,6 +545,154 @@ function isAllowedImageHost(url) {
545
545
  (allowed) => host === allowed || host.endsWith("." + allowed)
546
546
  );
547
547
  }
548
+ var FITS = ["regular", "oversize"];
549
+ function parseFit(v) {
550
+ const s = typeof v === "string" ? v.trim().toLowerCase() : "";
551
+ return s === "regular" || s === "oversize" ? s : null;
552
+ }
553
+ function fitLabel(f) {
554
+ return f === "oversize" ? "Oversize" : "Regular";
555
+ }
556
+ function variantFit(v) {
557
+ const opt = (v.options ?? []).find((o) => (o.option?.title ?? "").toLowerCase() === "fit");
558
+ return opt ? parseFit(opt.value) : null;
559
+ }
560
+ function hasFitAxis(p) {
561
+ return (p.variants ?? []).some((v) => variantFit(v) !== null);
562
+ }
563
+ function productFits(p) {
564
+ const seen = /* @__PURE__ */ new Set();
565
+ for (const v of p.variants ?? []) {
566
+ const f = variantFit(v);
567
+ if (f) seen.add(f);
568
+ }
569
+ return FITS.filter((f) => seen.has(f));
570
+ }
571
+ var BLANK_COLOURS = {
572
+ "gildan-5000": ["black", "white"],
573
+ "sols-03996": ["black", "white"],
574
+ "shaka-shmhss": ["black", "white", "khaki"]
575
+ };
576
+ function blankFor(fit, countryCode) {
577
+ if (fit === "regular") return "gildan-5000";
578
+ return (countryCode ?? "").trim().toLowerCase() === "us" ? "shaka-shmhss" : "sols-03996";
579
+ }
580
+ var BLANK_SIZES = {
581
+ "gildan-5000": ["s", "m", "l", "xl", "2xl", "3xl", "4xl", "5xl"],
582
+ "shaka-shmhss": ["s", "m", "l", "xl", "2xl", "3xl", "4xl", "5xl"],
583
+ "sols-03996": ["xs", "s", "m", "l", "xl", "2xl", "3xl"]
584
+ };
585
+ function sizeKey(size) {
586
+ const s = size.trim().toLowerCase();
587
+ return s === "xxl" ? "2xl" : s === "xxxl" ? "3xl" : s;
588
+ }
589
+ function colourBuyable(fit, colour, countryCode) {
590
+ return BLANK_COLOURS[blankFor(fit, countryCode)].includes(colour.trim().toLowerCase());
591
+ }
592
+ function sizeBuyable(fit, size, countryCode) {
593
+ return BLANK_SIZES[blankFor(fit, countryCode)].includes(sizeKey(size));
594
+ }
595
+ function legacyFit(p) {
596
+ const md = p.metadata ?? {};
597
+ const hay = [typeof md.fit === "string" ? md.fit : "", p.title ?? "", p.handle ?? ""].join(" ");
598
+ return /oversiz|drop.?shoulder/i.test(hay) ? "oversize" : "regular";
599
+ }
600
+ function gateFit(p, fit) {
601
+ if (fit) return fit;
602
+ return hasFitAxis(p) ? null : legacyFit(p);
603
+ }
604
+ function variantsOfFit(p, fit) {
605
+ const all = p.variants ?? [];
606
+ return fit === null ? all : all.filter((v) => variantFit(v) === fit);
607
+ }
608
+ function coloursFor(p, fit, countryCode) {
609
+ const gate = gateFit(p, fit);
610
+ const out = [];
611
+ for (const v of variantsOfFit(p, fit)) {
612
+ const c = variantColour(v);
613
+ if (!c || out.includes(c)) continue;
614
+ if (gate && countryCode !== void 0 && !colourBuyable(gate, c, countryCode)) continue;
615
+ out.push(c);
616
+ }
617
+ return out;
618
+ }
619
+ function sizesFor(p, fit, colour, countryCode) {
620
+ const gate = gateFit(p, fit);
621
+ const target = colour.trim().toLowerCase();
622
+ const out = [];
623
+ for (const v of variantsOfFit(p, fit)) {
624
+ if (variantColour(v) !== target) continue;
625
+ const s = sizeOfVariant(v);
626
+ if (!s || out.includes(s)) continue;
627
+ if (gate && countryCode !== void 0 && !sizeBuyable(gate, s, countryCode)) continue;
628
+ out.push(s);
629
+ }
630
+ return out;
631
+ }
632
+ function cheapest(vs) {
633
+ const priced = vs.filter((v2) => typeof v2.calculated_price?.calculated_amount === "number").sort(
634
+ (a, b) => a.calculated_price.calculated_amount - b.calculated_price.calculated_amount
635
+ );
636
+ const v = priced[0];
637
+ return {
638
+ amount: v?.calculated_price?.calculated_amount ?? null,
639
+ currency: v?.calculated_price?.currency_code ?? null,
640
+ taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null
641
+ };
642
+ }
643
+ function priceFrom(p) {
644
+ const fits = productFits(p);
645
+ const base = cheapest(p.variants ?? []);
646
+ const amounts = new Set(fits.map((f) => cheapest(variantsOfFit(p, f)).amount));
647
+ return { ...base, from: fits.length > 1 && amounts.size > 1 };
648
+ }
649
+ function priceFor(p, fit) {
650
+ return cheapest(variantsOfFit(p, fit));
651
+ }
652
+ function resolveVariant(p, opts) {
653
+ const fits = productFits(p);
654
+ const requested = parseFit(opts.fit);
655
+ const askedFor = typeof opts.fit === "string" ? opts.fit.trim() : "";
656
+ let fit = null;
657
+ if (fits.length > 0) {
658
+ if (requested && fits.includes(requested)) fit = requested;
659
+ else if (askedFor) return { ok: false, reason: "fit_not_offered", requested: askedFor, fits };
660
+ else if (fits.length === 1) fit = fits[0];
661
+ else return { ok: false, reason: "ambiguous_fit", fits: fits.map((f) => ({ fit: f, ...priceFor(p, f) })) };
662
+ }
663
+ const pool = variantsOfFit(p, fit);
664
+ const gate = gateFit(p, fit);
665
+ const colours = coloursFor(p, fit, opts.countryCode);
666
+ const colour = (opts.colour ?? "").trim().toLowerCase() || (colours[0] ?? null);
667
+ if (gate && colour && opts.countryCode !== void 0 && !colourBuyable(gate, colour, opts.countryCode)) {
668
+ return { ok: false, reason: "fit_unavailable", fit: gate, colour };
669
+ }
670
+ const size = (opts.size ?? "").trim() || null;
671
+ const sizeLc = size?.toLowerCase();
672
+ const buyableSizes = colour ? sizesFor(p, fit, colour, opts.countryCode) : [];
673
+ const sizeExists = !!size && pool.some((x) => (sizeOfVariant(x) ?? x.title ?? "").toLowerCase() === size.toLowerCase());
674
+ if (gate && size && sizeExists && opts.countryCode !== void 0 && !sizeBuyable(gate, size, opts.countryCode)) {
675
+ return { ok: false, reason: "size_unavailable", fit: gate, size, sizes: buyableSizes };
676
+ }
677
+ const sellable = gate && opts.countryCode !== void 0 ? pool.filter((x) => {
678
+ const s = sizeOfVariant(x);
679
+ return !s || sizeBuyable(gate, s, opts.countryCode);
680
+ }) : pool;
681
+ const v = sellable.find((x) => {
682
+ const colourOk = !colour || variantColour(x) === colour;
683
+ const sizeOk = !sizeLc || (sizeOfVariant(x) ?? "").toLowerCase() === sizeLc || (x.title ?? "").toLowerCase() === sizeLc;
684
+ return colourOk && sizeOk;
685
+ });
686
+ if (!v) {
687
+ return { ok: false, reason: "not_found", fit, colour, size, colours, sizes: buyableSizes };
688
+ }
689
+ return { ok: true, variant: v, fit, colour: variantColour(v), size: sizeOfVariant(v) };
690
+ }
691
+ var SIZE_TOKEN = /^(xxs|xs|s|m|l|xl|xxl|2xl|3xl|xxxl|4xl|5xl)$/i;
692
+ var FIT_TOKEN = /^(regular|oversize)$/i;
693
+ function titleSegments(title) {
694
+ return (title ?? "").split("/").map((s) => s.trim()).filter(Boolean);
695
+ }
548
696
  function displayColour(colour) {
549
697
  const c = colour.trim();
550
698
  if (!c) return c;
@@ -557,11 +705,10 @@ function variantColour(v) {
557
705
  if (opt && typeof opt.value === "string" && opt.value.trim()) {
558
706
  return opt.value.trim().toLowerCase();
559
707
  }
560
- const title = (v.title ?? "").trim();
561
- if (title.includes("/")) {
562
- const parts = title.split("/").map((p) => p.trim());
563
- const tail = parts[parts.length - 1];
564
- if (tail) return tail.toLowerCase();
708
+ const segs = titleSegments(v.title);
709
+ if (segs.length >= 2) {
710
+ const seg = segs.find((s) => !SIZE_TOKEN.test(s) && !FIT_TOKEN.test(s));
711
+ if (seg) return seg.toLowerCase();
565
712
  }
566
713
  return null;
567
714
  }
@@ -575,32 +722,28 @@ function productColours(p) {
575
722
  }
576
723
  return seen;
577
724
  }
578
- function sizesForColour(p, colour) {
579
- const target = colour.trim().toLowerCase();
580
- const sizes = [];
581
- for (const v of p.variants ?? []) {
582
- if (variantColour(v) !== target) continue;
583
- const size = sizeOfVariant(v);
584
- if (size && !sizes.includes(size)) {
585
- sizes.push(size);
586
- }
587
- }
588
- return sizes;
589
- }
590
725
  function sizeOfVariant(v) {
591
726
  const opt = (v.options ?? []).find((o) => (o.option?.title ?? "").toLowerCase() === "size");
592
727
  if (opt && typeof opt.value === "string" && opt.value.trim()) {
593
728
  return opt.value.trim();
594
729
  }
595
- const title = (v.title ?? "").trim();
596
- if (title.includes("/")) {
597
- const head = title.split("/")[0]?.trim();
598
- if (head) return head;
599
- }
600
- return title || null;
730
+ const segs = titleSegments(v.title);
731
+ const token = segs.find((s) => SIZE_TOKEN.test(s));
732
+ if (token) return token;
733
+ return segs.length === 1 ? segs[0] : segs[0] ?? null;
601
734
  }
602
735
  function findVariant(p, opts = {}) {
603
- const variants = p.variants ?? [];
736
+ const fits = productFits(p);
737
+ let fit = null;
738
+ if (fits.length > 0) {
739
+ const wanted = parseFit(opts.fit);
740
+ const askedFor = typeof opts.fit === "string" ? opts.fit.trim() : "";
741
+ if (wanted && fits.includes(wanted)) fit = wanted;
742
+ else if (askedFor) return void 0;
743
+ else if (fits.length === 1) fit = fits[0];
744
+ else return void 0;
745
+ }
746
+ const variants = (p.variants ?? []).filter((v) => fit === null || variantFit(v) === fit);
604
747
  const size = opts.size?.trim().toLowerCase();
605
748
  const colour = opts.colour?.trim().toLowerCase();
606
749
  const sizeMatches = (v) => {
@@ -660,6 +803,24 @@ function imageForColour(p, colour) {
660
803
  }
661
804
  return null;
662
805
  }
806
+ function imageFor(p, sel) {
807
+ const colour = sel.colour.trim().toLowerCase();
808
+ if (!colour) return null;
809
+ const metas = (p.images ?? []).map((img) => ({
810
+ url: (img?.url ?? "").trim(),
811
+ scene: typeof img?.metadata?.["scene"] === "string" ? String(img.metadata["scene"]) : null,
812
+ fit: parseFit(img?.metadata?.["fit"]),
813
+ colour: typeof img?.metadata?.["colour"] === "string" ? String(img.metadata["colour"]).toLowerCase() : null
814
+ }));
815
+ const tagged = metas.filter((m) => m.url && m.colour === colour && m.scene === PREFERRED_SCENE);
816
+ const exact = tagged.find((m) => sel.fit !== null && m.fit === sel.fit);
817
+ if (exact) return exact.url;
818
+ const neutral = tagged.find((m) => m.fit === null);
819
+ if (neutral) return neutral.url;
820
+ const otherFitOnly = tagged.length > 0 && sel.fit !== null;
821
+ if (otherFitOnly) return null;
822
+ return imageForColour(p, colour);
823
+ }
663
824
  function firstImageUrl(images) {
664
825
  for (const img of images ?? []) {
665
826
  const u = (img?.url ?? "").trim();
@@ -673,15 +834,19 @@ function escapeRegex(s) {
673
834
  function str(v, fallback) {
674
835
  return typeof v === "string" && v.length > 0 ? v : fallback;
675
836
  }
676
- function productSpecs(p) {
837
+ function productSpecs(p, fit) {
677
838
  const md = p.metadata ?? {};
839
+ const byFit = md.specs_by_fit ?? null;
840
+ const fitKey = fit ?? (byFit && Object.keys(byFit).length === 1 ? Object.keys(byFit)[0] : void 0);
841
+ const src = fitKey && byFit?.[fitKey] || md;
842
+ const pick = (k, fallback) => str(src[k] ?? md[k], fallback);
678
843
  return [
679
- { key: "material", value: str(md.material, "100% cotton") },
680
- { key: "weight", value: str(md.weight, "180 g/m\xB2") },
681
- { key: "fit", value: str(md.fit, "Unisex heavyweight crewneck") },
682
- { key: "print", value: str(md.print, "Front print") },
683
- { key: "delivery", value: str(md.delivery, "Ships in 2\u20135 business days + transit") },
684
- { key: "care", value: str(md.care, "Cold wash, line dry") }
844
+ { key: "material", value: pick("material", "100% cotton") },
845
+ { key: "weight", value: pick("weight", "180 g/m\xB2") },
846
+ { key: "fit", value: pick("fit", "Unisex heavyweight crewneck") },
847
+ { key: "print", value: pick("print", "Front print") },
848
+ { key: "delivery", value: pick("delivery", "Ships in 2\u20135 business days + transit") },
849
+ { key: "care", value: pick("care", "Cold wash, line dry") }
685
850
  ];
686
851
  }
687
852
  function productLore(p) {
@@ -891,6 +1056,7 @@ function colourSwatch(colour) {
891
1056
  const c = colour.trim().toLowerCase();
892
1057
  if (c === "white") return "\u2B1C";
893
1058
  if (c === "black") return "\u2B1B";
1059
+ if (c === "khaki") return "\u{1F7EB}";
894
1060
  return "\u25FC";
895
1061
  }
896
1062
  function colourMarker(colours) {
@@ -928,13 +1094,8 @@ function fmtPrice(amount, currency) {
928
1094
  function taxSuffix(isTaxInclusive) {
929
1095
  return isTaxInclusive ? " incl. VAT" : "";
930
1096
  }
931
- function firstPrice(p) {
932
- const v = (p.variants ?? [])[0];
933
- return {
934
- amount: v?.calculated_price?.calculated_amount ?? null,
935
- currency: v?.calculated_price?.currency_code ?? null,
936
- taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null
937
- };
1097
+ function priceLine(p) {
1098
+ return priceFrom(p);
938
1099
  }
939
1100
  function stageMarker(channelStage) {
940
1101
  return channelStage === "next" ? "\u25CB on next" : "\u25CF in stock";
@@ -944,7 +1105,14 @@ function displayName(p) {
944
1105
  const title = (p.title ?? "").toString().trim() || handle;
945
1106
  return { title, handle };
946
1107
  }
1108
+ function lineOption(item, titles) {
1109
+ const o = item.variant?.options?.find((x) => titles.includes((x.option?.title ?? "").toLowerCase()));
1110
+ const v = typeof o?.value === "string" ? o.value.trim() : "";
1111
+ return v ? v : null;
1112
+ }
947
1113
  function lineColour(item) {
1114
+ const opt = lineOption(item, ["colour", "color"]);
1115
+ if (opt) return opt.toLowerCase();
948
1116
  const vt = (item.variant_title ?? "").toString();
949
1117
  if (vt) {
950
1118
  const c = variantColour({ id: "x", title: vt });
@@ -952,6 +1120,15 @@ function lineColour(item) {
952
1120
  }
953
1121
  return null;
954
1122
  }
1123
+ function lineFit(item) {
1124
+ const opt = parseFit(lineOption(item, ["fit"]));
1125
+ if (opt) return opt;
1126
+ for (const seg of (item.variant_title ?? "").toString().split("/")) {
1127
+ const f = parseFit(seg);
1128
+ if (f) return f;
1129
+ }
1130
+ return null;
1131
+ }
955
1132
  function shortItemId(id) {
956
1133
  const s = (id ?? "").toString();
957
1134
  return s.length <= 6 ? s : s.slice(-6);
@@ -962,7 +1139,9 @@ function formatCartLine(item, index, currency, brand) {
962
1139
  const title = (item.title ?? item.product_title ?? "item").toString();
963
1140
  const idTag = item.id ? dimTag(style, ` (#${shortItemId(item.id)})`) : "";
964
1141
  const colour = lineColour(item);
965
- const colourTag = colour ? ` \xB7 ${colour}` : "";
1142
+ const fit = lineFit(item);
1143
+ const tags = [colour, fit ? fitLabel(fit).toLowerCase() : null].filter(Boolean).join(", ");
1144
+ const colourTag = tags ? ` \xB7 ${tags}` : "";
966
1145
  const price = item.total != null ? ` ${accentPrice(style, fmtPrice(item.total, currency))}` : "";
967
1146
  return ` ${ref} ${title}${colourTag}${idTag} \xB7 qty ${item.quantity}${price}`;
968
1147
  }
@@ -1032,6 +1211,17 @@ async function resolveProduct(client, region, selector) {
1032
1211
  if (exact) {
1033
1212
  return { product: exact };
1034
1213
  }
1214
+ const m = needle.match(/^(.+)-oversize$/i);
1215
+ if (m && m[1]) {
1216
+ const base = await client.getProduct(m[1], { region });
1217
+ if (base) {
1218
+ return {
1219
+ product: base,
1220
+ impliedFit: "oversize",
1221
+ aliasNote: `"${needle}" is a retired handle \xB7 resolved to ${base.handle ?? m[1]} --fit oversize`
1222
+ };
1223
+ }
1224
+ }
1035
1225
  }
1036
1226
  const products = await client.listProducts({ region });
1037
1227
  const r = resolveProductSelector(products, needle);
@@ -1172,47 +1362,70 @@ async function runView(client, config, args) {
1172
1362
  });
1173
1363
  }
1174
1364
  const { title, handle } = displayName(product);
1175
- const colours = productColours(product);
1365
+ const fits = productFits(product);
1366
+ const requestedFit = parseFit(args.fit ?? picked.impliedFit);
1367
+ if (args.fit && (!requestedFit || !fits.includes(requestedFit))) {
1368
+ return failResult({
1369
+ code: EXIT.DATAERR,
1370
+ what: fits.length ? `no fit called "${args.fit}" for ${handle}.` : `${handle} comes in one fit; --fit does not apply.`,
1371
+ fix: fits.length ? `bt view ${handle} --fit ${fits.join(" | ")}` : `bt view ${handle}`
1372
+ });
1373
+ }
1374
+ const fit = fits.length ? requestedFit ?? (fits.includes("regular") ? "regular" : fits[0]) : null;
1375
+ const countryCode = (rr.requested ?? region.countries?.[0]?.iso_2 ?? "").toLowerCase() || null;
1376
+ const colours = coloursFor(product, fit, countryCode);
1377
+ const unavailable = coloursFor(product, fit).filter((c) => !colours.includes(c));
1176
1378
  const lines = [];
1177
1379
  if (picked.fuzzy && handle !== args.handle.trim()) {
1178
1380
  lines.push(working(`matched "${handle}" from "${args.handle}"`));
1179
1381
  }
1382
+ if (picked.aliasNote) {
1383
+ lines.push(working(picked.aliasNote));
1384
+ }
1180
1385
  lines.push(header(style, title));
1181
1386
  lines.push(` ${dim(style, handle)}`);
1182
1387
  if (product.description) {
1183
1388
  lines.push(empty(` ${product.description}`));
1184
1389
  }
1185
1390
  const fabric = product.metadata?.["fabric"] ?? null;
1186
- const specs = productSpecs(product);
1391
+ if (fit && fits.length > 1) {
1392
+ const other = fits.find((f) => f !== fit);
1393
+ const op = priceFor(product, other);
1394
+ lines.push(` fits ${fits.map((f) => fitLabel(f)).join(" \xB7 ")} \u2192 viewing ${fitLabel(fit)} \xB7 the other \u2192 bt view ${handle} --fit ${other} (${fmtPrice(op.amount, op.currency)})`);
1395
+ }
1396
+ const specs = productSpecs(product, fit);
1187
1397
  for (const s of specs) {
1188
1398
  lines.push(` ${s.key.padEnd(9)}${s.value}`);
1189
1399
  }
1190
- if (colours.length > 0) {
1400
+ if (colours.length > 0 || unavailable.length > 0) {
1191
1401
  lines.push(` colours ${colours.map((c) => `${colourSwatch(c)} ${c}`).join(" \xB7 ")}`);
1192
1402
  for (const c of colours) {
1193
- const sizes = sortSizes(sizesForColour(product, c));
1403
+ const sizes = sortSizes(sizesFor(product, fit, c, countryCode));
1194
1404
  lines.push(` ${colourSwatch(c)} ${c.padEnd(6)} ${sizes.join(" ")}`);
1195
1405
  }
1406
+ for (const c of unavailable) {
1407
+ lines.push(dim(style, ` ${colourSwatch(c)} ${c.padEnd(6)} not available where you ship (${countryCode ?? "your region"})`));
1408
+ }
1196
1409
  } else {
1197
1410
  const sizes = sortSizes((product.variants ?? []).map((v) => v.title ?? "?"));
1198
1411
  lines.push(` sizes ${sizes.join(" ")}`);
1199
1412
  }
1200
- const v0 = (product.variants ?? [])[0];
1201
- if (v0?.calculated_price) {
1413
+ const pr = fit ? { ...priceFor(product, fit), from: false } : priceLine(product);
1414
+ if (pr.amount != null) {
1202
1415
  lines.push(
1203
- ` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`
1416
+ ` price ${cyan(style, `${pr.from ? "from " : ""}${fmtPrice(pr.amount, pr.currency)}`)}${taxSuffix(pr.taxInclusive)}`
1204
1417
  );
1205
1418
  lines.push(` ${regionLine(rr, style, config.region_source)}`);
1206
1419
  }
1207
1420
  let imageUrl = product.thumbnail ?? null;
1208
1421
  let blackNote = null;
1209
- if (args.colour) {
1210
- const wanted = args.colour.trim().toLowerCase();
1211
- const resolved = imageForColour(product, wanted);
1422
+ const wanted = args.colour?.trim().toLowerCase() || (fit ? colours[0] : void 0);
1423
+ if (wanted) {
1424
+ const resolved = imageFor(product, { fit, colour: wanted }) ?? (fit === null ? imageForColour(product, wanted) : null);
1212
1425
  if (resolved) {
1213
1426
  imageUrl = resolved;
1214
- } else {
1215
- blackNote = ` ${displayColour(wanted)} available \u2014 showing the default mockup (no ${wanted} render on file).`;
1427
+ } else if (args.colour) {
1428
+ blackNote = unavailable.includes(wanted) ? ` ${displayColour(wanted)} isn't available where you ship \u2014 showing the default mockup.` : ` ${displayColour(wanted)} available \u2014 showing the default mockup (no ${wanted} render on file).`;
1216
1429
  }
1217
1430
  }
1218
1431
  if (imageUrl) {
@@ -1224,7 +1437,7 @@ async function runView(client, config, args) {
1224
1437
  }
1225
1438
  if (colours.length > 1 && !args.colour) {
1226
1439
  const others = colours.filter((c) => c !== colours[0]).join(", ");
1227
- lines.push(` see another colour \u2192 bt view ${handle} --colour ${others.split(",")[0]?.trim()}`);
1440
+ lines.push(` see another colour \u2192 bt view ${handle} --colour ${others.split(",")[0]?.trim()}${fit && fits.length > 1 ? ` --fit ${fit}` : ""}`);
1228
1441
  }
1229
1442
  if (productLore(product).text) {
1230
1443
  lines.push(` the story \u2192 bt blame ${handle}`);
@@ -1237,11 +1450,17 @@ async function runView(client, config, args) {
1237
1450
  title: product.title,
1238
1451
  fabric,
1239
1452
  specs,
1453
+ fits,
1454
+ fit,
1240
1455
  colours,
1456
+ unavailable_colours: unavailable,
1241
1457
  thumbnail: product.thumbnail,
1458
+ // CAT-31: `size` is the REAL size (never the variant title); fit/colour from the options.
1242
1459
  variants: (product.variants ?? []).map((v) => ({
1243
1460
  id: v.id,
1244
- size: v.title,
1461
+ fit: variantFit(v),
1462
+ colour: variantColour(v),
1463
+ size: sizeOfVariant(v),
1245
1464
  price: v.calculated_price?.calculated_amount ?? null,
1246
1465
  currency: v.calculated_price?.currency_code ?? null
1247
1466
  }))
@@ -1249,12 +1468,13 @@ async function runView(client, config, args) {
1249
1468
  };
1250
1469
  }
1251
1470
  function registerView(program, makeClient, loadConfig) {
1252
- program.command("view").alias("show").argument("<handle>", "the design: a slug, a bt browse number (3), or a fuzzy hint (termnl)").option("--colour <colour>", "render a specific colour's mockup (e.g. black)").description("render a design: specs + image (always with the link as the floor).").action(async (handle, opts) => {
1471
+ program.command("view").alias("show").argument("<handle>", "the design: a slug, a bt browse number (3), or a fuzzy hint (termnl)").option("--colour <colour>", "render a specific colour's mockup (e.g. black)").option("--fit <fit>", "regular or oversize: which fit's sizes, price and image to show (default: regular)").description("render a design: specs + image (always with the link as the floor).").action(async (handle, opts) => {
1253
1472
  const { json, brand } = resolveOutput(program);
1254
1473
  const noColor = program.opts().color === false;
1255
1474
  const res = await runView(makeClient(), loadConfig(), {
1256
1475
  handle,
1257
1476
  colour: opts.colour,
1477
+ fit: opts.fit,
1258
1478
  json,
1259
1479
  noColor,
1260
1480
  brand
@@ -1279,38 +1499,82 @@ async function runStage(client, config, args) {
1279
1499
  const resolved = await resolveProduct(client, region, args.handle);
1280
1500
  if (!resolved.product) {
1281
1501
  if (resolved.candidates && resolved.candidates.length > 1) {
1282
- const r = failResult({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` });
1283
- r.lines.push(...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`), " or be exact \u2192 bt stage <slug>");
1284
- return r;
1502
+ const r2 = failResult({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` });
1503
+ r2.lines.push(...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`), " or be exact \u2192 bt stage <slug>");
1504
+ return r2;
1285
1505
  }
1286
1506
  return failResult({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" });
1287
1507
  }
1288
1508
  const product = resolved.product;
1289
1509
  const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
1290
1510
  const size = args.size ?? config.defaultSize;
1291
- const colours = productColours(product);
1292
- const colour = args.colour ?? config.defaultColour ?? (colours.length > 0 ? colours[0] : void 0);
1293
- const variant = findVariant(product, { size, colour });
1294
- if (!variant) {
1295
- const have = colours.length ? ` available colours: ${colours.join(", ")}.` : "";
1511
+ const countryCode = (rr.requested ?? region.countries?.[0]?.iso_2 ?? "").toLowerCase() || null;
1512
+ const explicitFit = args.fit ?? resolved.impliedFit;
1513
+ const fitArg = explicitFit ?? (productFits(product).length > 1 ? config.defaultFit : void 0);
1514
+ const r = resolveVariant(product, { fit: fitArg, size, colour: args.colour ?? config.defaultColour, countryCode });
1515
+ const handleShown = product.handle ?? args.handle;
1516
+ if (!r.ok) {
1517
+ if (r.reason === "ambiguous_fit") {
1518
+ const fitList = r.fits.map((f) => `${f.fit} (${fmtPrice(f.amount, f.currency)})`).join(" \xB7 ");
1519
+ const res = failResult({
1520
+ code: EXIT.DATAERR,
1521
+ what: `${handleShown} comes in two fits: ${fitList}.`,
1522
+ money: "nothing staged, nothing charged.",
1523
+ fix: `bt stage ${handleShown} --fit ${r.fits[0].fit}${size ? ` --size ${size}` : ""}`
1524
+ });
1525
+ res.lines.push(` or set yours once \u2192 bt config set defaultFit ${r.fits[0].fit}`);
1526
+ return res;
1527
+ }
1528
+ if (r.reason === "fit_not_offered") {
1529
+ return failResult({
1530
+ code: EXIT.DATAERR,
1531
+ what: `${handleShown} isn't sold in "${r.requested}"${r.fits.length ? ` \u2014 it comes in ${r.fits.join(" \xB7 ")}` : ""}.`,
1532
+ money: "nothing staged, nothing charged.",
1533
+ fix: r.fits.length ? `bt stage ${handleShown} --fit ${r.fits[0]}${size ? ` --size ${size}` : ""}` : `bt stage ${handleShown}${size ? ` --size ${size}` : ""} (this design comes in one fit)`
1534
+ });
1535
+ }
1536
+ if (r.reason === "fit_unavailable") {
1537
+ return failResult({
1538
+ code: EXIT.DATAERR,
1539
+ what: `${r.colour} is not available in ${r.fit} where you ship (${countryCode ?? "your region"}).`,
1540
+ money: "nothing staged, nothing charged.",
1541
+ fix: `bt view ${handleShown} --fit ${r.fit} (to see the colours you can buy)`
1542
+ });
1543
+ }
1544
+ if (r.reason === "size_unavailable") {
1545
+ return failResult({
1546
+ code: EXIT.DATAERR,
1547
+ what: `${r.size} is not made in ${r.fit} where you ship (${countryCode ?? "your region"}).`,
1548
+ money: "nothing staged, nothing charged.",
1549
+ fix: r.sizes.length ? `bt stage ${handleShown} --fit ${r.fit} --size ${r.sizes[0]} (sizes here: ${r.sizes.join(" ")})` : `bt view ${handleShown} --fit ${r.fit} (to see the sizes you can buy)`
1550
+ });
1551
+ }
1552
+ const have = r.colours.length ? ` available colours: ${r.colours.join(", ")}.` : "";
1553
+ const haveSizes = r.sizes.length ? ` sizes: ${r.sizes.join(" ")}.` : "";
1296
1554
  return failResult({
1297
1555
  code: EXIT.DATAERR,
1298
- what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
1299
- fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
1556
+ what: `no variant for ${args.handle}${r.fit ? ` --fit ${r.fit}` : ""}${size ? `#size=${size}` : ""}${r.colour ? ` (${r.colour})` : ""}.`,
1557
+ fix: `bt view ${args.handle}${r.fit ? ` --fit ${r.fit}` : ""} (to see available sizes + colours)${have}${haveSizes}`
1300
1558
  });
1301
1559
  }
1560
+ const variant = r.variant;
1561
+ const colour = r.colour ?? void 0;
1562
+ const fit = r.fit;
1563
+ const refTags = [colour, fit].filter(Boolean).join(", ");
1564
+ const colourSuffix = refTags ? ` (${refTags})` : "";
1302
1565
  if (args.dryRun) {
1303
- const colourSuffix2 = colour ? ` (${colour})` : "";
1304
1566
  return {
1305
1567
  lines: [
1306
1568
  ...matchNote ? [matchNote] : [],
1307
- ok(`dry-run \u2014 would stage ${product.handle ?? args.handle}${size ? `#${size}` : ""}${colourSuffix2} (nothing staged).`),
1569
+ ...resolved.aliasNote ? [warn(resolved.aliasNote)] : [],
1570
+ ok(`dry-run \u2014 would stage ${product.handle ?? args.handle}${size ? `#${size}` : ""}${colourSuffix} (nothing staged).`),
1308
1571
  " run without --dry-run to acquire the inventory lock."
1309
1572
  ],
1310
1573
  json: args.json ? {
1311
1574
  dry_run: true,
1312
1575
  would_stage: {
1313
1576
  handle: product.handle ?? args.handle,
1577
+ fit: fit ?? null,
1314
1578
  size: size ?? null,
1315
1579
  colour: colour ?? null,
1316
1580
  variant_id: variant.id
@@ -1322,6 +1586,9 @@ async function runStage(client, config, args) {
1322
1586
  if (matchNote) {
1323
1587
  lines.push(matchNote);
1324
1588
  }
1589
+ if (resolved.aliasNote) {
1590
+ lines.push(warn(resolved.aliasNote));
1591
+ }
1325
1592
  let cartId = config.cart_id;
1326
1593
  let recovered = false;
1327
1594
  if (!cartId) {
@@ -1348,7 +1615,6 @@ async function runStage(client, config, args) {
1348
1615
  )
1349
1616
  );
1350
1617
  }
1351
- const colourSuffix = colour ? ` (${colour})` : "";
1352
1618
  lines.push(
1353
1619
  ok(
1354
1620
  `staged ${product.handle ?? args.handle}${size ? `#${size}` : ""}${colourSuffix} \xB7 inventory lock acquired (held 10m)`
@@ -1363,22 +1629,23 @@ async function runStage(client, config, args) {
1363
1629
  // persist the cart_id atomically BEFORE returning (the router calls config.save)
1364
1630
  save: { cart_id: cartId },
1365
1631
  lines,
1366
- json: args.json ? { cart_id: cartId, recovered, colour: colour ?? null, items: cart.items ?? [], total: cart.total ?? null } : void 0
1632
+ json: args.json ? { cart_id: cartId, recovered, fit: fit ?? null, colour: colour ?? null, items: cart.items ?? [], total: cart.total ?? null } : void 0
1367
1633
  };
1368
1634
  }
1369
1635
  function registerStage(program, makeClient, loadConfig) {
1370
- program.command("stage").alias("add").argument("<handle>", "the design: a slug (terminal-velocity), a bt browse number (3), or a fuzzy hint (termnl)").option("--size <size>", "garment size (default: your last commit's size)").option("--colour <colour>", "colour, e.g. white / black (default: the product's first colour)").option("--dry-run", "resolve the variant + show what would be staged, without staging").description("reserve stock \u2014 acquires an inventory lock, reserves nothing permanent.").action(async (handle, opts) => {
1636
+ program.command("stage").alias("add").argument("<handle>", "the design: a slug (terminal-velocity), a bt browse number (3), or a fuzzy hint (termnl)").option("--size <size>", "garment size (default: your last commit's size)").option("--colour <colour>", "colour, e.g. white / black (default: the product's first colour)").option("--fit <fit>", "regular or oversize (required when a design comes in both; default: your defaultFit)").option("--dry-run", "resolve the variant + show what would be staged, without staging").description("reserve stock \u2014 acquires an inventory lock, reserves nothing permanent.").action(async (handle, opts) => {
1371
1637
  const { json, brand } = resolveOutput(program);
1372
1638
  const res = await runStage(makeClient(), loadConfig(), {
1373
1639
  handle,
1374
1640
  size: opts.size,
1375
1641
  colour: opts.colour,
1642
+ fit: opts.fit,
1376
1643
  dryRun: opts.dryRun,
1377
1644
  json,
1378
1645
  brand
1379
1646
  });
1380
1647
  if (res.save) {
1381
- const { saveConfig } = await import("./config-WV4DTQXU.js");
1648
+ const { saveConfig } = await import("./config-L4X7G563.js");
1382
1649
  saveConfig(res.save);
1383
1650
  }
1384
1651
  printResult(res, json);
@@ -1392,8 +1659,12 @@ export {
1392
1659
  StoreClient,
1393
1660
  BT_DEFAULT_STORE_URL,
1394
1661
  BT_DEFAULT_PUBLISHABLE_KEY,
1662
+ fitLabel,
1663
+ productFits,
1664
+ coloursFor,
1665
+ sizesFor,
1666
+ priceFor,
1395
1667
  productColours,
1396
- sizesForColour,
1397
1668
  findVariant,
1398
1669
  productLore,
1399
1670
  isCartTaxInclusive,
@@ -1419,7 +1690,7 @@ export {
1419
1690
  brandOf,
1420
1691
  fmtPrice,
1421
1692
  taxSuffix,
1422
- firstPrice,
1693
+ priceLine,
1423
1694
  stageMarker,
1424
1695
  displayName,
1425
1696
  lineColour,
@@ -1434,4 +1705,4 @@ export {
1434
1705
  runStage,
1435
1706
  registerStage
1436
1707
  };
1437
- //# sourceMappingURL=chunk-BFMH7VCE.js.map
1708
+ //# sourceMappingURL=chunk-42NDSW24.js.map