@binaryandthread/cli 0.4.11 → 0.4.12

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.
@@ -31,7 +31,11 @@ function loadConfig() {
31
31
  }
32
32
  }
33
33
  function saveConfig(patch) {
34
- const merged = { ...loadConfig(), ...sanitize(patch) };
34
+ const raw = patch;
35
+ const merged = { ...loadConfig(), ...sanitize(raw) };
36
+ for (const key of ALLOWED_KEYS) {
37
+ if (raw[key] === null) delete merged[key];
38
+ }
35
39
  const dir = configDir();
36
40
  fs.mkdirSync(dir, { recursive: true });
37
41
  const target = configPath();
@@ -64,4 +68,4 @@ export {
64
68
  saveConfig,
65
69
  forgetConfig
66
70
  };
67
- //# sourceMappingURL=chunk-T7MFWTB7.js.map
71
+ //# sourceMappingURL=chunk-IUN3OOYY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["// Local config — ~/.config/bt/config.json.\n//\n// Holds ONLY non-sensitive fields (SECURITY G15): cart_id, email, region, defaults.\n// The auth TOKEN is NEVER here — it goes in the OS keychain in Wave 5. Persisting a\n// token in plaintext config would be the exact leak G15 forbids.\n//\n// Writes are ATOMIC (F-B1): write to config.json.tmp then fs.renameSync, so a crash\n// mid-write never corrupts the file (rename is atomic on POSIX + Windows same-volume).\n\nimport * as fs from \"node:fs\"\nimport * as os from \"node:os\"\nimport * as path from \"node:path\"\n\nexport interface BtConfig {\n cart_id?: string\n email?: string\n region?: string\n defaultSize?: string\n defaultColour?: string\n /** CAT-31: \"regular\" | \"oversize\", used when a design comes in both fits and no --fit is given. */\n defaultFit?: string\n defaultBranch?: string\n /** RUNTIME-ONLY (ENG-21): where the effective region came from this run — flag, saved config, or\n * a locale guess. Deliberately NOT in ALLOWED_KEYS, so saveConfig always strips it. */\n region_source?: \"flag\" | \"config\" | \"locale\"\n}\n\n// Persisted keys exclude the RUNTIME-ONLY fields (region_source) — typing ALLOWED_KEYS as the full\n// `keyof BtConfig` wrongly widened `key` to include region_source's literal union, so `out[key] = v`\n// (a string) failed to typecheck. PersistedKey narrows it to the string-valued, persistable fields.\nexport type PersistedKey = Exclude<keyof BtConfig, \"region_source\">\n\n/** A config patch: a string sets the key, `null` DELETES it (TB252: a paid cart's id must go), undefined leaves it. */\nexport type ConfigPatch = { [K in PersistedKey]?: string | null }\n\n// Only these keys are ever persisted — anything else (notably `token`) is dropped (G15).\nconst ALLOWED_KEYS: Array<PersistedKey> = [\n \"cart_id\",\n \"email\",\n \"region\",\n \"defaultSize\",\n \"defaultColour\",\n \"defaultFit\",\n \"defaultBranch\",\n]\n\n/** The config directory: $XDG_CONFIG_HOME/bt or ~/.config/bt. */\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME\n const base = xdg && xdg.trim() ? xdg : path.join(os.homedir(), \".config\")\n return path.join(base, \"bt\")\n}\n\nexport function configPath(): string {\n return path.join(configDir(), \"config.json\")\n}\n\n/** Load the config, or {} if none / unreadable. Never throws. */\nexport function loadConfig(): BtConfig {\n try {\n const raw = fs.readFileSync(configPath(), \"utf8\")\n const parsed = JSON.parse(raw) as Record<string, unknown>\n return sanitize(parsed)\n } catch {\n return {}\n }\n}\n\n/**\n * Merge-and-save the given fields. Strips any non-allowlisted key (G15). Atomic (F-B1). A value of\n * exactly `null` deletes that key (TB252: before this no path could clear cart_id, so a paid cart's\n * id outlived its order and `bt cart` listed the paid items as still staged).\n */\nexport function saveConfig(patch: ConfigPatch | Partial<BtConfig>): BtConfig {\n const raw = patch as Record<string, unknown>\n const merged = { ...loadConfig(), ...sanitize(raw) }\n for (const key of ALLOWED_KEYS) {\n if (raw[key] === null) delete merged[key]\n }\n const dir = configDir()\n fs.mkdirSync(dir, { recursive: true })\n const target = configPath()\n const tmp = target + \".tmp\"\n // write to a temp file then atomically rename (F-B1)\n fs.writeFileSync(tmp, JSON.stringify(merged, null, 2), { encoding: \"utf8\", mode: 0o600 })\n fs.renameSync(tmp, target)\n return merged\n}\n\n/** Wipe the local config file (G15 erasure — `bt forget`). Never throws. */\nexport function forgetConfig(): void {\n try {\n fs.rmSync(configPath(), { force: true })\n } catch {\n /* ignore */\n }\n}\n\n/** Keep only allowlisted, non-sensitive keys (drops `token` and anything unexpected). */\nfunction sanitize(obj: Record<string, unknown>): BtConfig {\n const out: BtConfig = {}\n for (const key of ALLOWED_KEYS) {\n const v = obj[key]\n if (typeof v === \"string\" && v.length > 0) {\n out[key] = v\n }\n }\n return out\n}\n"],"mappings":";;;AASA,YAAY,QAAQ;AACpB,YAAY,QAAQ;AACpB,YAAY,UAAU;AAyBtB,IAAM,eAAoC;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,SAAS,YAAoB;AAClC,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,OAAO,OAAO,IAAI,KAAK,IAAI,MAAW,UAAQ,WAAQ,GAAG,SAAS;AACxE,SAAY,UAAK,MAAM,IAAI;AAC7B;AAEO,SAAS,aAAqB;AACnC,SAAY,UAAK,UAAU,GAAG,aAAa;AAC7C;AAGO,SAAS,aAAuB;AACrC,MAAI;AACF,UAAM,MAAS,gBAAa,WAAW,GAAG,MAAM;AAChD,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,SAAS,MAAM;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAOO,SAAS,WAAW,OAAkD;AAC3E,QAAM,MAAM;AACZ,QAAM,SAAS,EAAE,GAAG,WAAW,GAAG,GAAG,SAAS,GAAG,EAAE;AACnD,aAAW,OAAO,cAAc;AAC9B,QAAI,IAAI,GAAG,MAAM,KAAM,QAAO,OAAO,GAAG;AAAA,EAC1C;AACA,QAAM,MAAM,UAAU;AACtB,EAAG,aAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,SAAS,WAAW;AAC1B,QAAM,MAAM,SAAS;AAErB,EAAG,iBAAc,KAAK,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AACxF,EAAG,cAAW,KAAK,MAAM;AACzB,SAAO;AACT;AAGO,SAAS,eAAqB;AACnC,MAAI;AACF,IAAG,UAAO,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EACzC,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,SAAS,KAAwC;AACxD,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,cAAc;AAC9B,UAAM,IAAI,IAAI,GAAG;AACjB,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,GAAG;AACzC,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -220,13 +220,19 @@ var StoreClient = class {
220
220
  } catch {
221
221
  parsed = {};
222
222
  }
223
- if (opts.cartRecovery && (res.status === 404 || res.status === 409 || String(parsed.type ?? "").includes("cart.completed") || /cart.*complete/i.test(String(parsed.message ?? "")))) {
223
+ const idempotencyConflict = res.status === 409 && parsed.code === "IDEMPOTENCY_CONFLICT";
224
+ const parsedType = String(parsed.type ?? "");
225
+ if (opts.cartRecovery && (res.status === 404 || parsedType === "cart_completed" || parsedType.includes("cart.completed") || /cart.*complete/i.test(String(parsed.message ?? "")))) {
224
226
  throw new RecoverableCartError(
225
227
  `cart is stale or completed (HTTP ${res.status})`,
226
228
  res.status
227
229
  );
228
230
  }
229
- if (res.status === 429) {
231
+ if (idempotencyConflict) {
232
+ retryAfterMs = parseRetryAfterMs(res);
233
+ lastErr = new StoreApiError(`request in flight (HTTP 409)`, 409, path);
234
+ this.logWarn("store.request.idempotency_conflict", { path, attempt: attempt + 1, retryAfterMs });
235
+ } else if (res.status === 429) {
230
236
  retryAfterMs = parseRetryAfterMs(res);
231
237
  lastErr = new StoreApiError(`rate limited (HTTP 429)`, 429, path);
232
238
  this.logWarn("store.request.rate_limited", {
@@ -964,6 +970,31 @@ function firstImageUrl(images) {
964
970
  function escapeRegex(s) {
965
971
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
966
972
  }
973
+ function sizeTokenOf(title) {
974
+ return titleSegments(title).find((s) => SIZE_TOKEN.test(s)) ?? null;
975
+ }
976
+ var SIZE_ORDER = ["xxs", "xs", "s", "m", "l", "xl", "2xl", "xxl", "3xl", "xxxl", "4xl", "5xl"];
977
+ function sortSizes(sizes) {
978
+ const rank = (s) => {
979
+ const i = SIZE_ORDER.indexOf(s.trim().toLowerCase());
980
+ return i === -1 ? SIZE_ORDER.length : i;
981
+ };
982
+ return [...sizes].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
983
+ }
984
+ function lineOption(item, titles) {
985
+ const o = item.variant?.options?.find((x) => titles.includes((x.option?.title ?? "").toLowerCase()));
986
+ const v = typeof o?.value === "string" ? o.value.trim() : "";
987
+ return v ? v : null;
988
+ }
989
+ function lineColour(item) {
990
+ const opt = lineOption(item, ["colour", "color"]);
991
+ if (opt) return opt.toLowerCase();
992
+ const vt = (item.variant_title ?? "").toString();
993
+ return vt ? variantColour({ id: "x", title: vt }) : null;
994
+ }
995
+ function lineSize(item) {
996
+ return lineOption(item, ["size"]) ?? sizeTokenOf(item.variant_title);
997
+ }
967
998
  function orderStage(o) {
968
999
  const status = (o.status ?? "").trim().toLowerCase();
969
1000
  if (status === "canceled" || status === "refunded") return "reverted";
@@ -1298,14 +1329,6 @@ function regionLine(rr, style, source) {
1298
1329
  const nudge = rr.home ? " \xB7 not you? \u2192 bt region" : source === "locale" ? " \xB7 guessed from your locale \u2014 not you? \u2192 bt region" : "";
1299
1330
  return dim(style, `${GLYPH.gear} region: ${loc}${rr.region.currency_code}${nudge}`);
1300
1331
  }
1301
- var SIZE_ORDER = ["xxs", "xs", "s", "m", "l", "xl", "2xl", "xxl", "3xl", "xxxl", "4xl", "5xl"];
1302
- function sortSizes(sizes) {
1303
- const rank = (s) => {
1304
- const i = SIZE_ORDER.indexOf(s.trim().toLowerCase());
1305
- return i === -1 ? SIZE_ORDER.length : i;
1306
- };
1307
- return [...sizes].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
1308
- }
1309
1332
  function brandOf(args) {
1310
1333
  return args?.brand ?? PLAIN;
1311
1334
  }
@@ -1330,21 +1353,6 @@ function displayName(p) {
1330
1353
  const title = (p.title ?? "").toString().trim() || handle;
1331
1354
  return { title, handle };
1332
1355
  }
1333
- function lineOption(item, titles) {
1334
- const o = item.variant?.options?.find((x) => titles.includes((x.option?.title ?? "").toLowerCase()));
1335
- const v = typeof o?.value === "string" ? o.value.trim() : "";
1336
- return v ? v : null;
1337
- }
1338
- function lineColour(item) {
1339
- const opt = lineOption(item, ["colour", "color"]);
1340
- if (opt) return opt.toLowerCase();
1341
- const vt = (item.variant_title ?? "").toString();
1342
- if (vt) {
1343
- const c = variantColour({ id: "x", title: vt });
1344
- if (c) return c;
1345
- }
1346
- return null;
1347
- }
1348
1356
  function lineFit(item) {
1349
1357
  const opt = parseFit(lineOption(item, ["fit"]));
1350
1358
  if (opt) return opt;
@@ -1365,7 +1373,8 @@ function formatCartLine(item, index, currency, brand) {
1365
1373
  const idTag = item.id ? dimTag(style, ` (#${shortItemId(item.id)})`) : "";
1366
1374
  const colour = lineColour(item);
1367
1375
  const fit = lineFit(item);
1368
- const tags = [colour ? colourLabel(colour).toLowerCase() : null, fit ? fitLabel(fit).toLowerCase() : null].filter(Boolean).join(", ");
1376
+ const size = lineSize(item);
1377
+ const tags = [colour ? colourLabel(colour).toLowerCase() : null, fit ? fitLabel(fit).toLowerCase() : null, size].filter(Boolean).join(", ");
1369
1378
  const colourTag = tags ? ` \xB7 ${tags}` : "";
1370
1379
  const price = item.total != null ? ` ${accentPrice(style, fmtPrice(item.total, currency))}` : "";
1371
1380
  return ` ${ref} ${title}${colourTag}${idTag} \xB7 qty ${item.quantity}${price}`;
@@ -1721,6 +1730,10 @@ function registerView(program, makeClient, loadConfig) {
1721
1730
  }
1722
1731
 
1723
1732
  // src/commands/stage.ts
1733
+ function sizeChoice(sizes) {
1734
+ const sorted = sortSizes(sizes);
1735
+ return sorted.length === 1 ? sorted[0] : `<${sorted.join("|")}>`;
1736
+ }
1724
1737
  async function runStage(client, config, args) {
1725
1738
  const style = brandOf(args);
1726
1739
  const rr = await client.resolveRegionResult(config.region);
@@ -1745,6 +1758,11 @@ async function runStage(client, config, args) {
1745
1758
  cartId = void 0;
1746
1759
  recovered = true;
1747
1760
  }
1761
+ if (existing?.completed_at) {
1762
+ existing = null;
1763
+ cartId = void 0;
1764
+ recovered = true;
1765
+ }
1748
1766
  if (existing) {
1749
1767
  const to = await cartShipTo(existing, () => client.getRegions());
1750
1768
  if (!to) {
@@ -1817,7 +1835,7 @@ async function runStage(client, config, args) {
1817
1835
  }
1818
1836
  const fitFlag = r.fit && productFits(product).length > 1 ? ` --fit ${r.fit}` : "";
1819
1837
  const colourAsked = "colour" in r ? r.colour : null;
1820
- const madeIn = coloursFor(product, r.fit ?? null);
1838
+ const madeIn = coloursFor(product, productFits(product).length > 0 ? r.fit ?? null : null);
1821
1839
  if (colourAsked && (r.reason === "fit_unavailable" || r.reason === "not_found") && !madeIn.includes(colourAsked)) {
1822
1840
  return failResult({
1823
1841
  code: EXIT.DATAERR,
@@ -1839,7 +1857,7 @@ async function runStage(client, config, args) {
1839
1857
  code: EXIT.DATAERR,
1840
1858
  what: `${r.size} is not made in ${r.fit} where you ship (${countryCode ?? "your region"}).`,
1841
1859
  money: "nothing staged, nothing charged.",
1842
- 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)`
1860
+ fix: r.sizes.length ? `${r.sizes.length > 1 ? "ask which size is wanted, don't choose for them, then: " : ""}bt stage ${handleShown} --fit ${r.fit} --size ${sizeChoice(r.sizes)}` : `bt view ${handleShown} --fit ${r.fit} (to see the sizes you can buy)`
1843
1861
  });
1844
1862
  }
1845
1863
  const inColour = r.colour ? ` in ${r.colour}` : "";
@@ -1848,7 +1866,7 @@ async function runStage(client, config, args) {
1848
1866
  code: EXIT.DATAERR,
1849
1867
  what: `${handleShown} doesn't come in size ${size}${inColour}. sizes: ${r.sizes.join(" ")}.`,
1850
1868
  money: "nothing staged, nothing charged.",
1851
- fix: `bt stage ${handleShown}${fitFlag} --size ${r.sizes[0]}${r.colour ? ` --colour ${r.colour}` : ""}`
1869
+ fix: `${r.sizes.length > 1 ? "ask which size is wanted, don't choose for them, then: " : ""}bt stage ${handleShown}${fitFlag} --size ${sizeChoice(r.sizes)}${r.colour ? ` --colour ${r.colour}` : ""}`
1852
1870
  });
1853
1871
  }
1854
1872
  return failResult({
@@ -1861,6 +1879,20 @@ async function runStage(client, config, args) {
1861
1879
  const variant = r.variant;
1862
1880
  const colour = r.colour ?? void 0;
1863
1881
  const fit = r.fit;
1882
+ if (!size) {
1883
+ const sizes = sortSizes(
1884
+ r.colour ? sizesFor(product, r.fit, r.colour, countryCode) : [...new Set((product.variants ?? []).map((v) => sizeOfVariant(v)).filter((s) => !!s))]
1885
+ );
1886
+ if (sizes.length > 1) {
1887
+ const fitFlag = r.fit && productFits(product).length > 1 ? ` --fit ${r.fit}` : "";
1888
+ return failResult({
1889
+ code: EXIT.DATAERR,
1890
+ what: `${handleShown} needs a size. sizes${r.colour ? ` in ${r.colour}` : ""}: ${sizes.join(" ")}.`,
1891
+ money: "nothing staged, nothing charged.",
1892
+ fix: `ask which size is wanted, don't choose for them, then: bt stage ${handleShown}${fitFlag} --size ${sizeChoice(sizes)}${r.colour ? ` --colour ${r.colour}` : ""}`
1893
+ });
1894
+ }
1895
+ }
1864
1896
  const refTags = [colour ? colourLabel(colour).toLowerCase() : null, fit].filter(Boolean).join(", ");
1865
1897
  const colourSuffix = refTags ? ` (${refTags})` : "";
1866
1898
  if (args.dryRun) {
@@ -1931,9 +1963,19 @@ async function runStage(client, config, args) {
1931
1963
  json: args.json ? { cart_id: cartId, recovered, fit: fit ?? null, colour: colour ?? null, items: cart.items ?? [], total: cart.total ?? null } : void 0
1932
1964
  };
1933
1965
  }
1966
+ function oneDesignPerCommand() {
1967
+ return failResult({
1968
+ code: EXIT.DATAERR,
1969
+ what: 'one design per command. a two-word colour is written --colour coal, or in quotes: --colour "faded coal"'
1970
+ });
1971
+ }
1934
1972
  function registerStage(program, makeClient, loadConfig) {
1935
- 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) => {
1973
+ 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 (required unless bt config set defaultSize)").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.").allowExcessArguments().action(async (handle, opts, cmd) => {
1936
1974
  const { json, brand } = resolveOutput(program);
1975
+ if (cmd.args.length > 1) {
1976
+ printResult(oneDesignPerCommand(), json);
1977
+ return;
1978
+ }
1937
1979
  const res = await runStage(makeClient(), loadConfig(), {
1938
1980
  handle,
1939
1981
  size: opts.size,
@@ -1944,7 +1986,7 @@ function registerStage(program, makeClient, loadConfig) {
1944
1986
  brand
1945
1987
  });
1946
1988
  if (res.save) {
1947
- const { saveConfig } = await import("./config-L4X7G563.js");
1989
+ const { saveConfig } = await import("./config-PWI2G2O5.js");
1948
1990
  saveConfig(res.save);
1949
1991
  }
1950
1992
  printResult(res, json);
@@ -1967,6 +2009,7 @@ export {
1967
2009
  labelVariantTitle,
1968
2010
  productColours,
1969
2011
  findVariant,
2012
+ lineColour,
1970
2013
  orderStage,
1971
2014
  productLore,
1972
2015
  isCartTaxInclusive,
@@ -1996,7 +2039,6 @@ export {
1996
2039
  priceLine,
1997
2040
  stageMarker,
1998
2041
  displayName,
1999
- lineColour,
2000
2042
  formatCartLine,
2001
2043
  resolveLineItemRef,
2002
2044
  resolveProduct,
@@ -2007,6 +2049,7 @@ export {
2007
2049
  runView,
2008
2050
  registerView,
2009
2051
  runStage,
2052
+ oneDesignPerCommand,
2010
2053
  registerStage
2011
2054
  };
2012
- //# sourceMappingURL=chunk-UTSTDWKZ.js.map
2055
+ //# sourceMappingURL=chunk-YNBNKCCN.js.map