@binaryandthread/cli 0.1.3 → 0.1.5

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.
@@ -17,7 +17,7 @@ function makePrompts() {
17
17
  async function confirm(label) {
18
18
  const rl = readline.createInterface({ input, output });
19
19
  try {
20
- const answer = (await rl.question(`\u26A0 ${label} `)).trim().toLowerCase();
20
+ const answer = (await rl.question(` ${label} `)).trim().toLowerCase();
21
21
  return answer === "y" || answer === "yes";
22
22
  } finally {
23
23
  rl.close();
@@ -29,4 +29,4 @@ function makePrompts() {
29
29
  export {
30
30
  makePrompts
31
31
  };
32
- //# sourceMappingURL=chunk-IM3HF7NN.js.map
32
+ //# sourceMappingURL=chunk-AAN4VWP3.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/prompts.ts"],"sourcesContent":["// Terminal prompts — the real readline-backed prompter for the interactive flows.\n//\n// Kept tiny + dependency-free (node:readline/promises). The command handlers take a prompts\n// object INJECTED (CheckoutPrompts), so this concrete implementation is only wired by bin.ts;\n// the flows are unit-tested with a fake prompter. NOTE: this prompter NEVER asks for a card —\n// the only PII it collects is an email + shipping address (cards live on the storefront page).\n\nimport * as readline from \"node:readline/promises\"\nimport { stdin as input, stdout as output } from \"node:process\"\n\nexport interface Prompts {\n ask(label: string, opts?: { default?: string }): Promise<string>\n confirm(label: string): Promise<boolean>\n}\n\n/** Build a readline-backed prompter. Each call opens + closes its own interface (simple, safe). */\nexport function makePrompts(): Prompts {\n async function ask(label: string, opts?: { default?: string }): Promise<string> {\n const rl = readline.createInterface({ input, output })\n try {\n const suffix = opts?.default ? ` [${opts.default}]` : \"\"\n const answer = (await rl.question(` ${label}${suffix}: `)).trim()\n return answer || (opts?.default ?? \"\")\n } finally {\n rl.close()\n }\n }\n\n async function confirm(label: string): Promise<boolean> {\n const rl = readline.createInterface({ input, output })\n try {\n const answer = (await rl.question(`⚠ ${label} `)).trim().toLowerCase()\n return answer === \"y\" || answer === \"yes\"\n } finally {\n rl.close()\n }\n }\n\n return { ask, confirm }\n}\n"],"mappings":";;;AAOA,YAAY,cAAc;AAC1B,SAAS,SAAS,OAAO,UAAU,cAAc;AAQ1C,SAAS,cAAuB;AACrC,iBAAe,IAAI,OAAe,MAA8C;AAC9E,UAAM,KAAc,yBAAgB,EAAE,OAAO,OAAO,CAAC;AACrD,QAAI;AACF,YAAM,SAAS,MAAM,UAAU,KAAK,KAAK,OAAO,MAAM;AACtD,YAAM,UAAU,MAAM,GAAG,SAAS,MAAM,KAAK,GAAG,MAAM,IAAI,GAAG,KAAK;AAClE,aAAO,WAAW,MAAM,WAAW;AAAA,IACrC,UAAE;AACA,SAAG,MAAM;AAAA,IACX;AAAA,EACF;AAEA,iBAAe,QAAQ,OAAiC;AACtD,UAAM,KAAc,yBAAgB,EAAE,OAAO,OAAO,CAAC;AACrD,QAAI;AACF,YAAM,UAAU,MAAM,GAAG,SAAS,WAAM,KAAK,GAAG,GAAG,KAAK,EAAE,YAAY;AACtE,aAAO,WAAW,OAAO,WAAW;AAAA,IACtC,UAAE;AACA,SAAG,MAAM;AAAA,IACX;AAAA,EACF;AAEA,SAAO,EAAE,KAAK,QAAQ;AACxB;","names":[]}
1
+ {"version":3,"sources":["../src/prompts.ts"],"sourcesContent":["// Terminal prompts — the real readline-backed prompter for the interactive flows.\n//\n// Kept tiny + dependency-free (node:readline/promises). The command handlers take a prompts\n// object INJECTED (CheckoutPrompts), so this concrete implementation is only wired by bin.ts;\n// the flows are unit-tested with a fake prompter. NOTE: this prompter NEVER asks for a card —\n// the only PII it collects is an email + shipping address (cards live on the storefront page).\n\nimport * as readline from \"node:readline/promises\"\nimport { stdin as input, stdout as output } from \"node:process\"\n\nexport interface Prompts {\n ask(label: string, opts?: { default?: string }): Promise<string>\n confirm(label: string): Promise<boolean>\n}\n\n/** Build a readline-backed prompter. Each call opens + closes its own interface (simple, safe). */\nexport function makePrompts(): Prompts {\n async function ask(label: string, opts?: { default?: string }): Promise<string> {\n const rl = readline.createInterface({ input, output })\n try {\n const suffix = opts?.default ? ` [${opts.default}]` : \"\"\n const answer = (await rl.question(` ${label}${suffix}: `)).trim()\n return answer || (opts?.default ?? \"\")\n } finally {\n rl.close()\n }\n }\n\n async function confirm(label: string): Promise<boolean> {\n const rl = readline.createInterface({ input, output })\n try {\n // A neutral question marker — NOT ⚠. The confirms we ask (proceed to checkout, send a report)\n // are benign: nothing is charged, nothing is destroyed. Reserve ⚠ for real warnings so the\n // voice never cries wolf; a caller that truly needs a warning puts ⚠ in its own label.\n const answer = (await rl.question(` ${label} `)).trim().toLowerCase()\n return answer === \"y\" || answer === \"yes\"\n } finally {\n rl.close()\n }\n }\n\n return { ask, confirm }\n}\n"],"mappings":";;;AAOA,YAAY,cAAc;AAC1B,SAAS,SAAS,OAAO,UAAU,cAAc;AAQ1C,SAAS,cAAuB;AACrC,iBAAe,IAAI,OAAe,MAA8C;AAC9E,UAAM,KAAc,yBAAgB,EAAE,OAAO,OAAO,CAAC;AACrD,QAAI;AACF,YAAM,SAAS,MAAM,UAAU,KAAK,KAAK,OAAO,MAAM;AACtD,YAAM,UAAU,MAAM,GAAG,SAAS,MAAM,KAAK,GAAG,MAAM,IAAI,GAAG,KAAK;AAClE,aAAO,WAAW,MAAM,WAAW;AAAA,IACrC,UAAE;AACA,SAAG,MAAM;AAAA,IACX;AAAA,EACF;AAEA,iBAAe,QAAQ,OAAiC;AACtD,UAAM,KAAc,yBAAgB,EAAE,OAAO,OAAO,CAAC;AACrD,QAAI;AAIF,YAAM,UAAU,MAAM,GAAG,SAAS,MAAM,KAAK,GAAG,GAAG,KAAK,EAAE,YAAY;AACtE,aAAO,WAAW,OAAO,WAAW;AAAA,IACtC,UAAE;AACA,SAAG,MAAM;AAAA,IACX;AAAA,EACF;AAEA,SAAO,EAAE,KAAK,QAAQ;AACxB;","names":[]}
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../teetime-backend/packages/store-client/dist/index.js
3
+ // ../store-client/dist/index.js
4
4
  var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
5
5
  "authorization",
6
6
  "token",
@@ -44,15 +44,21 @@ function redact(input, _seen) {
44
44
  if (typeof input !== "object") {
45
45
  return input;
46
46
  }
47
- const seen = _seen ?? /* @__PURE__ */ new WeakSet();
48
- if (seen.has(input)) {
49
- return input;
47
+ const seen = _seen ?? /* @__PURE__ */ new Map();
48
+ const existing = seen.get(input);
49
+ if (existing !== void 0) {
50
+ return existing;
50
51
  }
51
- seen.add(input);
52
52
  if (Array.isArray(input)) {
53
- return input.map((v) => redact(v, seen));
53
+ const out2 = [];
54
+ seen.set(input, out2);
55
+ for (const v of input) {
56
+ out2.push(redact(v, seen));
57
+ }
58
+ return out2;
54
59
  }
55
60
  const out = {};
61
+ seen.set(input, out);
56
62
  for (const [key, value] of Object.entries(input)) {
57
63
  if (SENSITIVE_KEYS.has(key.toLowerCase())) {
58
64
  out[key] = MASK;
@@ -70,6 +76,7 @@ var DEFAULT_RETRY = {
70
76
  maxDelayMs: 2e3
71
77
  };
72
78
  var RETRY_AFTER_MAX_MS = 1e4;
79
+ var DEFAULT_TIMEOUT_MS = 15e3;
73
80
  function parseRetryAfterMs(res) {
74
81
  let raw = null;
75
82
  try {
@@ -134,6 +141,7 @@ var StoreClient = class {
134
141
  logger;
135
142
  retry;
136
143
  sleepImpl;
144
+ timeoutMs;
137
145
  /** Cached resolved regions (read-only catalog optimisation). */
138
146
  regionsCache = null;
139
147
  constructor(opts) {
@@ -153,6 +161,7 @@ var StoreClient = class {
153
161
  this.logger = opts.logger ?? console;
154
162
  this.retry = { ...DEFAULT_RETRY, ...opts.retry ?? {} };
155
163
  this.sleepImpl = opts.sleepImpl ?? sleep;
164
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
156
165
  }
157
166
  // ----- the request engine --------------------------------------------------
158
167
  buildUrl(path, query) {
@@ -195,8 +204,10 @@ var StoreClient = class {
195
204
  let lastErr;
196
205
  for (let attempt = 0; attempt < this.retry.attempts; attempt++) {
197
206
  let retryAfterMs = null;
207
+ const controller = new AbortController();
208
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
198
209
  try {
199
- const res = await this.fetchImpl(url, init);
210
+ const res = await this.fetchImpl(url, { ...init, signal: controller.signal });
200
211
  if (res.ok) {
201
212
  const text = await res.text();
202
213
  return text ? JSON.parse(text) : {};
@@ -244,6 +255,8 @@ var StoreClient = class {
244
255
  }
245
256
  lastErr = e;
246
257
  this.logWarn("store.request.network_retry", { path, attempt: attempt + 1 });
258
+ } finally {
259
+ clearTimeout(timer);
247
260
  }
248
261
  if (attempt < this.retry.attempts - 1) {
249
262
  await this.sleepImpl(retryAfterMs ?? this.backoffMs(attempt));
@@ -294,10 +307,20 @@ var StoreClient = class {
294
307
  // ----- products (region resolved BEFORE pricing) ---------------------------
295
308
  async listProducts(opts) {
296
309
  const region = opts?.region ?? await this.resolveRegion(opts?.countryCode);
297
- const data = await this.request("GET", "/store/products", {
298
- query: { region_id: region.id, limit: 100 }
299
- });
300
- return data.products ?? [];
310
+ const PAGE = 100;
311
+ const HARD_CAP = 1e3;
312
+ const all = [];
313
+ for (let offset = 0; offset < HARD_CAP; offset += PAGE) {
314
+ const data = await this.request("GET", "/store/products", {
315
+ query: { region_id: region.id, limit: PAGE, offset }
316
+ });
317
+ const page = data.products ?? [];
318
+ all.push(...page);
319
+ if (page.length < PAGE) {
320
+ break;
321
+ }
322
+ }
323
+ return all;
301
324
  }
302
325
  async getProduct(handle, opts) {
303
326
  const region = opts?.region ?? await this.resolveRegion(opts?.countryCode);
@@ -388,7 +411,9 @@ var StoreClient = class {
388
411
  return res.cart;
389
412
  }
390
413
  // ----- headless contract (04.5-01) -----------------------------------------
391
- async mintCheckout(cartId, email, channel = "cli") {
414
+ // `channel` is REQUIRED (L11): a default silently mis-attributes analytics for a future caller that
415
+ // forgets it. Both current callers (cli, mcp) pass it explicitly.
416
+ async mintCheckout(cartId, email, channel) {
392
417
  return this.request("POST", "/store/headless/checkout", {
393
418
  idempotent: true,
394
419
  body: { cart_id: cartId, email, channel }
@@ -461,13 +486,15 @@ var StoreClient = class {
461
486
  }
462
487
  }
463
488
  /**
464
- * Guest order lookup — requires order display id + EXACT email (F-H1).
465
- * The backend returns 404 on a mismatch (no enumeration); the client surfaces null.
489
+ * Guest order lookup (AUD-8) — POST /store/order-lookup with the order id (display id OR internal
490
+ * id) + the EXACT email in the BODY (never the query string, so the email is not access-logged).
491
+ * The backend returns 404 on any mismatch (no enumeration) → the client surfaces null. Rate-limited
492
+ * server-side; returns minimal status only, never PII (F-H1 / F-H3).
466
493
  */
467
- async getOrder(displayId, email) {
494
+ async getOrder(idOrDisplayId, email) {
468
495
  try {
469
- return await this.request("GET", "/store/orders", {
470
- query: { display_id: String(displayId), email }
496
+ return await this.request("POST", "/store/order-lookup", {
497
+ body: { id: String(idOrDisplayId), email }
471
498
  });
472
499
  } catch (e) {
473
500
  if (e instanceof StoreApiError && e.status === 404) {
@@ -567,7 +594,7 @@ function findVariant(p, opts = {}) {
567
594
  return variants.find((v) => colourMatches(v));
568
595
  }
569
596
  if (size && !colour) {
570
- return variants.find((v) => sizeMatches(v)) ?? variants[0];
597
+ return variants.find((v) => sizeMatches(v));
571
598
  }
572
599
  return variants[0];
573
600
  }
@@ -604,6 +631,29 @@ function firstImageUrl(images) {
604
631
  function escapeRegex(s) {
605
632
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
606
633
  }
634
+ function str(v, fallback) {
635
+ return typeof v === "string" && v.length > 0 ? v : fallback;
636
+ }
637
+ function productSpecs(p) {
638
+ const md = p.metadata ?? {};
639
+ return [
640
+ { key: "material", value: str(md.material, "100% cotton") },
641
+ { key: "weight", value: str(md.weight, "180 g/m\xB2") },
642
+ { key: "fit", value: str(md.fit, "Unisex heavyweight crewneck") },
643
+ { key: "print", value: str(md.print, "Front print") },
644
+ { key: "delivery", value: str(md.delivery, "Ships in 2\u20135 business days + transit") },
645
+ { key: "care", value: str(md.care, "Cold wash, line dry") }
646
+ ];
647
+ }
648
+ function productLore(p) {
649
+ const md = p.metadata ?? {};
650
+ const text = typeof md.lore === "string" ? md.lore : "";
651
+ const raw = Array.isArray(md.lore_sources) ? md.lore_sources : [];
652
+ const sources = raw.filter(
653
+ (s) => !!s && typeof s === "object" && typeof s.url === "string" && typeof s.label === "string"
654
+ ).map((s) => ({ url: s.url, label: s.label }));
655
+ return { text, sources };
656
+ }
607
657
 
608
658
  // src/voice.ts
609
659
  var GLYPH = {
@@ -646,15 +696,67 @@ var CODENAMES = [
646
696
  "tabs-over-spaces",
647
697
  "vim-not-emacs",
648
698
  "2-space-indent",
649
- "trailing-comma"
699
+ "trailing-comma",
700
+ "rebase-not-merge",
701
+ "lf-not-crlf",
702
+ "semicolons-optional",
703
+ "dark-mode-only"
650
704
  ];
651
- function codename(_version) {
652
- return CODENAMES[0];
705
+ function codename(version) {
706
+ const minor = Number.parseInt(version.split(".")[1] ?? "", 10);
707
+ const idx = Number.isFinite(minor) ? Math.abs(minor) % CODENAMES.length : 0;
708
+ return CODENAMES[idx];
653
709
  }
654
710
  function versionLine(version) {
655
711
  return `bt ${version} "${codename(version)}"`;
656
712
  }
657
713
 
714
+ // src/errors.ts
715
+ var EXIT = {
716
+ OK: 0,
717
+ GENERIC: 1,
718
+ DATAERR: 65,
719
+ // EX_DATAERR — bad request / unprocessable / stale cart (400/409/422)
720
+ UNAVAILABLE: 69,
721
+ // EX_UNAVAILABLE — not found (404)
722
+ TEMPFAIL: 75,
723
+ // EX_TEMPFAIL — temporary: 5xx, network/exhausted, rate-limited (429)
724
+ NOPERM: 77
725
+ // EX_NOPERM — auth required / forbidden (401/403)
726
+ };
727
+ function messageOf(e) {
728
+ if (e instanceof Error && e.message) {
729
+ return e.message;
730
+ }
731
+ const s = String(e ?? "").trim();
732
+ return s || "unexpected error";
733
+ }
734
+ function classifyError(e) {
735
+ const message = String(redact(messageOf(e)));
736
+ if (e instanceof RecoverableCartError) {
737
+ return { code: EXIT.DATAERR, type: "stale_cart", status: e.status, message };
738
+ }
739
+ if (e instanceof StoreApiError) {
740
+ const s = e.status;
741
+ if (s === 401 || s === 403) return { code: EXIT.NOPERM, type: "auth", status: s, message };
742
+ if (s === 404) return { code: EXIT.UNAVAILABLE, type: "not_found", status: s, message };
743
+ if (s === 429) return { code: EXIT.TEMPFAIL, type: "rate_limited", status: s, message };
744
+ if (s === 0 || s === 408 || s >= 500) return { code: EXIT.TEMPFAIL, type: "upstream", status: s, message };
745
+ if (s >= 400) return { code: EXIT.DATAERR, type: "client_error", status: s, message };
746
+ }
747
+ return { code: EXIT.GENERIC, type: "error", message };
748
+ }
749
+ function formatFatal(e, json) {
750
+ const info = classifyError(e);
751
+ if (json) {
752
+ const envelope = { error: { code: info.code, type: info.type, message: info.message } };
753
+ return { out: JSON.stringify(envelope, null, 2), err: null, code: info.code };
754
+ }
755
+ const reassurance = "nothing was lost. if a checkout was in flight, your order is safe. file it \u2192 bt report";
756
+ return { out: null, err: `\u2717 ${info.message}
757
+ ${reassurance}`, code: info.code };
758
+ }
759
+
658
760
  // src/brand.ts
659
761
  var ANSI = {
660
762
  reset: "\x1B[0m",
@@ -828,6 +930,53 @@ function resolveLineItemRef(items, ref) {
828
930
  );
829
931
  return byTitle ?? null;
830
932
  }
933
+ function isSubsequence(needle, hay) {
934
+ let i = 0;
935
+ for (let j = 0; j < hay.length && i < needle.length; j++) {
936
+ if (hay[j] === needle[i]) i++;
937
+ }
938
+ return i === needle.length;
939
+ }
940
+ function resolveProductSelector(products, selector) {
941
+ const needle = (selector ?? "").trim();
942
+ if (!needle || products.length === 0) {
943
+ return { candidates: [], fuzzy: false };
944
+ }
945
+ if (/^\d+$/.test(needle)) {
946
+ const idx = Number(needle) - 1;
947
+ if (idx >= 0 && idx < products.length) {
948
+ return { match: products[idx], candidates: [], fuzzy: true };
949
+ }
950
+ return { candidates: [], fuzzy: true };
951
+ }
952
+ const lower = needle.toLowerCase();
953
+ const exact = products.find((p) => (p.handle ?? "").toLowerCase() === lower || p.id === needle);
954
+ if (exact) {
955
+ return { match: exact, candidates: [], fuzzy: false };
956
+ }
957
+ const hits = products.filter(
958
+ (p) => isSubsequence(lower, (p.handle ?? "").toLowerCase()) || isSubsequence(lower, (p.title ?? "").toLowerCase())
959
+ );
960
+ if (hits.length === 1) {
961
+ return { match: hits[0], candidates: [], fuzzy: true };
962
+ }
963
+ return { candidates: hits, fuzzy: true };
964
+ }
965
+ async function resolveProduct(client, region, selector) {
966
+ const needle = (selector ?? "").trim();
967
+ if (needle && !/^\d+$/.test(needle)) {
968
+ const exact = await client.getProduct(needle, { region });
969
+ if (exact) {
970
+ return { product: exact };
971
+ }
972
+ }
973
+ const products = await client.listProducts({ region });
974
+ const r = resolveProductSelector(products, needle);
975
+ if (r.match) {
976
+ return { product: r.match, fuzzy: r.fuzzy };
977
+ }
978
+ return { candidates: r.candidates, fuzzy: r.fuzzy };
979
+ }
831
980
  function resolveOutput(program) {
832
981
  const opts = program.opts();
833
982
  const json = Boolean(opts.json);
@@ -835,6 +984,9 @@ function resolveOutput(program) {
835
984
  return { json, brand };
836
985
  }
837
986
  function printResult(res, json) {
987
+ if (res.exitCode) {
988
+ process.exitCode = res.exitCode;
989
+ }
838
990
  if (json && res.json !== void 0) {
839
991
  process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
840
992
  return;
@@ -912,29 +1064,45 @@ async function defaultRender(url, opts) {
912
1064
  async function runView(client, config, args) {
913
1065
  const style = brandOf(args);
914
1066
  const region = await client.resolveRegion(config.region);
915
- const product = await client.getProduct(args.handle, { region });
1067
+ const picked = await resolveProduct(client, region, args.handle);
1068
+ const product = picked.product;
916
1069
  if (!product) {
1070
+ if (picked.candidates && picked.candidates.length > 1) {
1071
+ return {
1072
+ lines: [
1073
+ fail({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${picked.candidates.length} designs \u2014 pick one:` }),
1074
+ ...picked.candidates.map((p) => ` ${p.handle ?? p.id}`),
1075
+ " or be exact \u2192 bt view <slug>"
1076
+ ],
1077
+ exitCode: EXIT.DATAERR
1078
+ };
1079
+ }
917
1080
  return {
918
1081
  lines: [
919
1082
  fail({
920
- code: 1,
921
- what: `variant ${args.handle} not found.`,
1083
+ code: EXIT.UNAVAILABLE,
1084
+ what: `no design here called "${args.handle}".`,
922
1085
  fix: "bt browse (to see what's in stock)"
923
1086
  })
924
- ]
1087
+ ],
1088
+ exitCode: EXIT.UNAVAILABLE
925
1089
  };
926
1090
  }
927
1091
  const { title, handle } = displayName(product);
928
1092
  const colours = productColours(product);
929
1093
  const lines = [];
1094
+ if (picked.fuzzy && handle !== args.handle.trim()) {
1095
+ lines.push(working(`matched "${handle}" from "${args.handle}"`));
1096
+ }
930
1097
  lines.push(header(style, title));
931
1098
  lines.push(` ${dim(style, handle)}`);
932
1099
  if (product.description) {
933
1100
  lines.push(empty(` ${product.description}`));
934
1101
  }
935
1102
  const fabric = product.metadata?.["fabric"] ?? null;
936
- if (fabric) {
937
- lines.push(` fabric ${fabric}`);
1103
+ const specs = productSpecs(product);
1104
+ for (const s of specs) {
1105
+ lines.push(` ${s.key.padEnd(9)}${s.value}`);
938
1106
  }
939
1107
  if (colours.length > 0) {
940
1108
  lines.push(` colours ${colours.map((c) => `${colourSwatch(c)} ${c}`).join(" \xB7 ")}`);
@@ -981,6 +1149,7 @@ async function runView(client, config, args) {
981
1149
  handle: product.handle,
982
1150
  title: product.title,
983
1151
  fabric,
1152
+ specs,
984
1153
  colours,
985
1154
  thumbnail: product.thumbnail,
986
1155
  variants: (product.variants ?? []).map((v) => ({
@@ -993,7 +1162,7 @@ async function runView(client, config, args) {
993
1162
  };
994
1163
  }
995
1164
  function registerView(program, makeClient, loadConfig) {
996
- program.command("view").alias("show").argument("<handle>", "the product handle/slug").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) => {
1165
+ 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) => {
997
1166
  const { json, brand } = resolveOutput(program);
998
1167
  const noColor = program.opts().color === false;
999
1168
  const res = await runView(makeClient(), loadConfig(), {
@@ -1011,14 +1180,29 @@ function registerView(program, makeClient, loadConfig) {
1011
1180
  async function runStage(client, config, args) {
1012
1181
  const style = brandOf(args);
1013
1182
  const region = await client.resolveRegion(config.region);
1014
- const product = await client.getProduct(args.handle, { region });
1015
- if (!product) {
1183
+ const resolved = await resolveProduct(client, region, args.handle);
1184
+ if (!resolved.product) {
1185
+ if (resolved.candidates && resolved.candidates.length > 1) {
1186
+ return {
1187
+ lines: [
1188
+ fail({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` }),
1189
+ ...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`),
1190
+ " or be exact \u2192 bt stage <slug>"
1191
+ ],
1192
+ exitCode: EXIT.DATAERR
1193
+ // ambiguous input → non-zero so `bt stage x && bt checkout` halts (d10)
1194
+ };
1195
+ }
1016
1196
  return {
1017
1197
  lines: [
1018
- fail({ code: 1, what: `variant ${args.handle} not found.`, fix: "bt browse" })
1019
- ]
1198
+ fail({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" })
1199
+ ],
1200
+ exitCode: EXIT.UNAVAILABLE
1201
+ // not-found → non-zero (d10)
1020
1202
  };
1021
1203
  }
1204
+ const product = resolved.product;
1205
+ const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
1022
1206
  const size = args.size ?? config.defaultSize;
1023
1207
  const colours = productColours(product);
1024
1208
  const colour = args.colour ?? config.defaultColour ?? (colours.length > 0 ? colours[0] : void 0);
@@ -1028,17 +1212,20 @@ async function runStage(client, config, args) {
1028
1212
  return {
1029
1213
  lines: [
1030
1214
  fail({
1031
- code: 1,
1215
+ code: EXIT.DATAERR,
1032
1216
  what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
1033
1217
  fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
1034
1218
  })
1035
- ]
1219
+ ],
1220
+ exitCode: EXIT.DATAERR
1221
+ // unprocessable size+colour combo → non-zero (d10)
1036
1222
  };
1037
1223
  }
1038
1224
  if (args.dryRun) {
1039
1225
  const colourSuffix2 = colour ? ` (${colour})` : "";
1040
1226
  return {
1041
1227
  lines: [
1228
+ ...matchNote ? [matchNote] : [],
1042
1229
  ok(`dry-run \u2014 would stage ${product.handle ?? args.handle}${size ? `#${size}` : ""}${colourSuffix2} (nothing staged).`),
1043
1230
  " run without --dry-run to acquire the inventory lock."
1044
1231
  ],
@@ -1054,6 +1241,9 @@ async function runStage(client, config, args) {
1054
1241
  };
1055
1242
  }
1056
1243
  const lines = [];
1244
+ if (matchNote) {
1245
+ lines.push(matchNote);
1246
+ }
1057
1247
  let cartId = config.cart_id;
1058
1248
  let recovered = false;
1059
1249
  if (!cartId) {
@@ -1099,7 +1289,7 @@ async function runStage(client, config, args) {
1099
1289
  };
1100
1290
  }
1101
1291
  function registerStage(program, makeClient, loadConfig) {
1102
- program.command("stage").alias("add").argument("<handle>", "the product handle/slug").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) => {
1292
+ 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) => {
1103
1293
  const { json, brand } = resolveOutput(program);
1104
1294
  const res = await runStage(makeClient(), loadConfig(), {
1105
1295
  handle,
@@ -1127,6 +1317,7 @@ export {
1127
1317
  productColours,
1128
1318
  sizesForColour,
1129
1319
  findVariant,
1320
+ productLore,
1130
1321
  GLYPH,
1131
1322
  ok,
1132
1323
  warn,
@@ -1134,6 +1325,8 @@ export {
1134
1325
  empty,
1135
1326
  fail,
1136
1327
  versionLine,
1328
+ EXIT,
1329
+ formatFatal,
1137
1330
  cyan,
1138
1331
  dim,
1139
1332
  green,
@@ -1151,6 +1344,7 @@ export {
1151
1344
  lineColour,
1152
1345
  formatCartLine,
1153
1346
  resolveLineItemRef,
1347
+ resolveProduct,
1154
1348
  resolveOutput,
1155
1349
  printResult,
1156
1350
  runView,
@@ -1158,4 +1352,4 @@ export {
1158
1352
  runStage,
1159
1353
  registerStage
1160
1354
  };
1161
- //# sourceMappingURL=chunk-7FH7BZ3I.js.map
1355
+ //# sourceMappingURL=chunk-VRNJLVHX.js.map