@binaryandthread/cli 0.1.9 → 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.
@@ -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");
@@ -1067,30 +1131,21 @@ async function defaultRender(url, opts) {
1067
1131
  // src/commands/view.ts
1068
1132
  async function runView(client, config, args) {
1069
1133
  const style = brandOf(args);
1070
- const region = await client.resolveRegion(config.region);
1134
+ const rr = await client.resolveRegionResult(config.region);
1135
+ const region = rr.region;
1071
1136
  const picked = await resolveProduct(client, region, args.handle);
1072
1137
  const product = picked.product;
1073
1138
  if (!product) {
1074
1139
  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
- };
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;
1083
1143
  }
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
- };
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
+ });
1094
1149
  }
1095
1150
  const { title, handle } = displayName(product);
1096
1151
  const colours = productColours(product);
@@ -1111,18 +1166,19 @@ async function runView(client, config, args) {
1111
1166
  if (colours.length > 0) {
1112
1167
  lines.push(` colours ${colours.map((c) => `${colourSwatch(c)} ${c}`).join(" \xB7 ")}`);
1113
1168
  for (const c of colours) {
1114
- const sizes = sizesForColour(product, c);
1169
+ const sizes = sortSizes(sizesForColour(product, c));
1115
1170
  lines.push(` ${colourSwatch(c)} ${c.padEnd(6)} ${sizes.join(" ")}`);
1116
1171
  }
1117
1172
  } else {
1118
- const sizes = (product.variants ?? []).map((v) => v.title ?? "?");
1173
+ const sizes = sortSizes((product.variants ?? []).map((v) => v.title ?? "?"));
1119
1174
  lines.push(` sizes ${sizes.join(" ")}`);
1120
1175
  }
1121
1176
  const v0 = (product.variants ?? [])[0];
1122
1177
  if (v0?.calculated_price) {
1123
1178
  lines.push(
1124
- ` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))} incl. VAT`
1179
+ ` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`
1125
1180
  );
1181
+ lines.push(` ${regionLine(rr, style)}`);
1126
1182
  }
1127
1183
  let imageUrl = product.thumbnail ?? null;
1128
1184
  let blackNote = null;
@@ -1186,27 +1242,24 @@ function registerView(program, makeClient, loadConfig) {
1186
1242
  // src/commands/stage.ts
1187
1243
  async function runStage(client, config, args) {
1188
1244
  const style = brandOf(args);
1189
- const region = await client.resolveRegion(config.region);
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;
1190
1255
  const resolved = await resolveProduct(client, region, args.handle);
1191
1256
  if (!resolved.product) {
1192
1257
  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
- };
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;
1202
1261
  }
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
- };
1262
+ return failResult({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" });
1210
1263
  }
1211
1264
  const product = resolved.product;
1212
1265
  const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
@@ -1216,17 +1269,11 @@ async function runStage(client, config, args) {
1216
1269
  const variant = findVariant(product, { size, colour });
1217
1270
  if (!variant) {
1218
1271
  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
- };
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
+ });
1230
1277
  }
1231
1278
  if (args.dryRun) {
1232
1279
  const colourSuffix2 = colour ? ` (${colour})` : "";
@@ -1325,6 +1372,7 @@ export {
1325
1372
  sizesForColour,
1326
1373
  findVariant,
1327
1374
  productLore,
1375
+ isCartTaxInclusive,
1328
1376
  GLYPH,
1329
1377
  ok,
1330
1378
  warn,
@@ -1343,8 +1391,10 @@ export {
1343
1391
  contributionBar,
1344
1392
  colourSwatch,
1345
1393
  colourMarker,
1394
+ regionLine,
1346
1395
  brandOf,
1347
1396
  fmtPrice,
1397
+ taxSuffix,
1348
1398
  firstPrice,
1349
1399
  stageMarker,
1350
1400
  displayName,
@@ -1353,10 +1403,11 @@ export {
1353
1403
  resolveLineItemRef,
1354
1404
  resolveProduct,
1355
1405
  resolveOutput,
1406
+ failResult,
1356
1407
  printResult,
1357
1408
  runView,
1358
1409
  registerView,
1359
1410
  runStage,
1360
1411
  registerStage
1361
1412
  };
1362
- //# sourceMappingURL=chunk-RGXH3EZ6.js.map
1413
+ //# sourceMappingURL=chunk-NJ3DW5AX.js.map