@binaryandthread/cli 0.1.9 → 0.2.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.
@@ -286,25 +286,45 @@ var StoreClient = class {
286
286
  return this.regionsCache;
287
287
  }
288
288
  /**
289
- * Resolve a region by country code, falling back to region #1.
290
- * SEK is region #1 by configuration — NEVER a hardcoded literal.
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 resolveRegion(countryCode) {
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
  }
305
- this.logWarn("store.region.unmatched", { countryCode: cc, fallback: regions[0].id });
316
+ this.logWarn("store.region.unmatched", { countryCode: cc, fallback: home.id });
317
+ return { region: home, matched: false, requested: cc, home: false };
306
318
  }
307
- return regions[0];
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;
308
328
  }
309
329
  // ----- products (region resolved BEFORE pricing) ---------------------------
310
330
  async listProducts(opts) {
@@ -658,6 +678,9 @@ function productLore(p) {
658
678
  ).map((s) => ({ url: s.url, label: s.label }));
659
679
  return { text, sources };
660
680
  }
681
+ function isCartTaxInclusive(cart) {
682
+ return (cart?.items ?? []).some((i) => i.is_tax_inclusive === true);
683
+ }
661
684
 
662
685
  // src/voice.ts
663
686
  var GLYPH = {
@@ -806,12 +829,15 @@ var BOX = {
806
829
  ml: "\u251C",
807
830
  mr: "\u2524"
808
831
  };
832
+ function cursor(style) {
833
+ return cyan(style, "_");
834
+ }
809
835
  function header(style, title, width = 56) {
810
836
  const label = ` ${title} `;
811
837
  const lead = BOX.h.repeat(2);
812
838
  const used = lead.length + label.length;
813
- const tail = BOX.h.repeat(Math.max(2, width - used));
814
- 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)}`;
815
841
  }
816
842
  function rule(width = 56) {
817
843
  return BOX.h.repeat(width);
@@ -860,6 +886,20 @@ function colourMarker(colours) {
860
886
  }
861
887
 
862
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
+ }
863
903
  function brandOf(args) {
864
904
  return args?.brand ?? PLAIN;
865
905
  }
@@ -870,11 +910,15 @@ function fmtPrice(amount, currency) {
870
910
  const cur = (currency ?? "").toUpperCase();
871
911
  return cur ? `${amount} ${cur}` : `${amount}`;
872
912
  }
913
+ function taxSuffix(isTaxInclusive) {
914
+ return isTaxInclusive ? " incl. VAT" : "";
915
+ }
873
916
  function firstPrice(p) {
874
917
  const v = (p.variants ?? [])[0];
875
918
  return {
876
919
  amount: v?.calculated_price?.calculated_amount ?? null,
877
- 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
878
922
  };
879
923
  }
880
924
  function stageMarker(channelStage) {
@@ -987,12 +1031,32 @@ function resolveOutput(program) {
987
1031
  const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor: opts.color === false });
988
1032
  return { json, brand };
989
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
+ }
990
1050
  function printResult(res, json) {
991
1051
  if (res.exitCode) {
992
1052
  process.exitCode = res.exitCode;
993
1053
  }
994
- if (json && res.json !== void 0) {
995
- process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
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
+ }
996
1060
  return;
997
1061
  }
998
1062
  process.stdout.write(res.lines.join("\n") + "\n");
@@ -1030,6 +1094,14 @@ ${urlFloor(blobUrl)}`;
1030
1094
  }
1031
1095
  var IMAGE_FETCH_TIMEOUT_MS = 5e3;
1032
1096
  var IMAGE_BYTE_CAP = 2 * 1024 * 1024;
1097
+ function isWebp(buf) {
1098
+ return buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP";
1099
+ }
1100
+ async function toPng(buf) {
1101
+ if (!isWebp(buf)) return buf;
1102
+ const sharp = (await import("sharp")).default;
1103
+ return sharp(buf).png().toBuffer();
1104
+ }
1033
1105
  async function defaultRender(url, opts) {
1034
1106
  const mod = await import("terminal-image");
1035
1107
  const terminalImage = mod.default ?? mod;
@@ -1053,10 +1125,11 @@ async function defaultRender(url, opts) {
1053
1125
  if (Number.isFinite(declaredLen) && declaredLen > cap) {
1054
1126
  throw new Error("blob exceeds byte cap (declared)");
1055
1127
  }
1056
- const buf = Buffer.from(await res.arrayBuffer());
1057
- if (buf.byteLength === 0 || buf.byteLength > cap) {
1128
+ const rawBuf = Buffer.from(await res.arrayBuffer());
1129
+ if (rawBuf.byteLength === 0 || rawBuf.byteLength > cap) {
1058
1130
  throw new Error("blob empty or exceeds byte cap");
1059
1131
  }
1132
+ const buf = await toPng(rawBuf);
1060
1133
  return bufferFn(buf, {
1061
1134
  height: opts.height ?? DEFAULT_RENDER_HEIGHT,
1062
1135
  preserveAspectRatio: true,
@@ -1067,30 +1140,21 @@ async function defaultRender(url, opts) {
1067
1140
  // src/commands/view.ts
1068
1141
  async function runView(client, config, args) {
1069
1142
  const style = brandOf(args);
1070
- const region = await client.resolveRegion(config.region);
1143
+ const rr = await client.resolveRegionResult(config.region);
1144
+ const region = rr.region;
1071
1145
  const picked = await resolveProduct(client, region, args.handle);
1072
1146
  const product = picked.product;
1073
1147
  if (!product) {
1074
1148
  if (picked.candidates && picked.candidates.length > 1) {
1075
- return {
1076
- lines: [
1077
- fail({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${picked.candidates.length} designs \u2014 pick one:` }),
1078
- ...picked.candidates.map((p) => ` ${p.handle ?? p.id}`),
1079
- " or be exact \u2192 bt view <slug>"
1080
- ],
1081
- exitCode: EXIT.DATAERR
1082
- };
1149
+ const r = failResult({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${picked.candidates.length} designs \u2014 pick one:` });
1150
+ r.lines.push(...picked.candidates.map((p) => ` ${p.handle ?? p.id}`), " or be exact \u2192 bt view <slug>");
1151
+ return r;
1083
1152
  }
1084
- return {
1085
- lines: [
1086
- fail({
1087
- code: EXIT.UNAVAILABLE,
1088
- what: `no design here called "${args.handle}".`,
1089
- fix: "bt browse (to see what's in stock)"
1090
- })
1091
- ],
1092
- exitCode: EXIT.UNAVAILABLE
1093
- };
1153
+ return failResult({
1154
+ code: EXIT.UNAVAILABLE,
1155
+ what: `no design here called "${args.handle}".`,
1156
+ fix: "bt browse (to see what's in stock)"
1157
+ });
1094
1158
  }
1095
1159
  const { title, handle } = displayName(product);
1096
1160
  const colours = productColours(product);
@@ -1111,18 +1175,19 @@ async function runView(client, config, args) {
1111
1175
  if (colours.length > 0) {
1112
1176
  lines.push(` colours ${colours.map((c) => `${colourSwatch(c)} ${c}`).join(" \xB7 ")}`);
1113
1177
  for (const c of colours) {
1114
- const sizes = sizesForColour(product, c);
1178
+ const sizes = sortSizes(sizesForColour(product, c));
1115
1179
  lines.push(` ${colourSwatch(c)} ${c.padEnd(6)} ${sizes.join(" ")}`);
1116
1180
  }
1117
1181
  } else {
1118
- const sizes = (product.variants ?? []).map((v) => v.title ?? "?");
1182
+ const sizes = sortSizes((product.variants ?? []).map((v) => v.title ?? "?"));
1119
1183
  lines.push(` sizes ${sizes.join(" ")}`);
1120
1184
  }
1121
1185
  const v0 = (product.variants ?? [])[0];
1122
1186
  if (v0?.calculated_price) {
1123
1187
  lines.push(
1124
- ` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))} incl. VAT`
1188
+ ` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`
1125
1189
  );
1190
+ lines.push(` ${regionLine(rr, style)}`);
1126
1191
  }
1127
1192
  let imageUrl = product.thumbnail ?? null;
1128
1193
  let blackNote = null;
@@ -1186,27 +1251,24 @@ function registerView(program, makeClient, loadConfig) {
1186
1251
  // src/commands/stage.ts
1187
1252
  async function runStage(client, config, args) {
1188
1253
  const style = brandOf(args);
1189
- const region = await client.resolveRegion(config.region);
1254
+ const rr = await client.resolveRegionResult(config.region);
1255
+ if (rr.requested && !rr.matched) {
1256
+ return failResult({
1257
+ code: EXIT.DATAERR,
1258
+ what: `we don't ship to ${rr.requested} yet.`,
1259
+ money: "nothing staged, nothing charged.",
1260
+ fix: "bt region --list (see where we ship, then bt region <cc>)"
1261
+ });
1262
+ }
1263
+ const region = rr.region;
1190
1264
  const resolved = await resolveProduct(client, region, args.handle);
1191
1265
  if (!resolved.product) {
1192
1266
  if (resolved.candidates && resolved.candidates.length > 1) {
1193
- return {
1194
- lines: [
1195
- fail({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` }),
1196
- ...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`),
1197
- " or be exact \u2192 bt stage <slug>"
1198
- ],
1199
- exitCode: EXIT.DATAERR
1200
- // ambiguous input → non-zero so `bt stage x && bt checkout` halts (d10)
1201
- };
1267
+ const r = failResult({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` });
1268
+ r.lines.push(...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`), " or be exact \u2192 bt stage <slug>");
1269
+ return r;
1202
1270
  }
1203
- return {
1204
- lines: [
1205
- fail({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" })
1206
- ],
1207
- exitCode: EXIT.UNAVAILABLE
1208
- // not-found → non-zero (d10)
1209
- };
1271
+ return failResult({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" });
1210
1272
  }
1211
1273
  const product = resolved.product;
1212
1274
  const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
@@ -1216,17 +1278,11 @@ async function runStage(client, config, args) {
1216
1278
  const variant = findVariant(product, { size, colour });
1217
1279
  if (!variant) {
1218
1280
  const have = colours.length ? ` available colours: ${colours.join(", ")}.` : "";
1219
- return {
1220
- lines: [
1221
- fail({
1222
- code: EXIT.DATAERR,
1223
- what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
1224
- fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
1225
- })
1226
- ],
1227
- exitCode: EXIT.DATAERR
1228
- // unprocessable size+colour combo → non-zero (d10)
1229
- };
1281
+ return failResult({
1282
+ code: EXIT.DATAERR,
1283
+ what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
1284
+ fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
1285
+ });
1230
1286
  }
1231
1287
  if (args.dryRun) {
1232
1288
  const colourSuffix2 = colour ? ` (${colour})` : "";
@@ -1325,6 +1381,7 @@ export {
1325
1381
  sizesForColour,
1326
1382
  findVariant,
1327
1383
  productLore,
1384
+ isCartTaxInclusive,
1328
1385
  GLYPH,
1329
1386
  ok,
1330
1387
  warn,
@@ -1343,8 +1400,10 @@ export {
1343
1400
  contributionBar,
1344
1401
  colourSwatch,
1345
1402
  colourMarker,
1403
+ regionLine,
1346
1404
  brandOf,
1347
1405
  fmtPrice,
1406
+ taxSuffix,
1348
1407
  firstPrice,
1349
1408
  stageMarker,
1350
1409
  displayName,
@@ -1353,10 +1412,11 @@ export {
1353
1412
  resolveLineItemRef,
1354
1413
  resolveProduct,
1355
1414
  resolveOutput,
1415
+ failResult,
1356
1416
  printResult,
1357
1417
  runView,
1358
1418
  registerView,
1359
1419
  runStage,
1360
1420
  registerStage
1361
1421
  };
1362
- //# sourceMappingURL=chunk-RGXH3EZ6.js.map
1422
+ //# sourceMappingURL=chunk-AP5ARNWU.js.map