@binaryandthread/cli 0.1.3 → 0.1.4

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
  }
@@ -646,15 +673,67 @@ var CODENAMES = [
646
673
  "tabs-over-spaces",
647
674
  "vim-not-emacs",
648
675
  "2-space-indent",
649
- "trailing-comma"
676
+ "trailing-comma",
677
+ "rebase-not-merge",
678
+ "lf-not-crlf",
679
+ "semicolons-optional",
680
+ "dark-mode-only"
650
681
  ];
651
- function codename(_version) {
652
- return CODENAMES[0];
682
+ function codename(version) {
683
+ const minor = Number.parseInt(version.split(".")[1] ?? "", 10);
684
+ const idx = Number.isFinite(minor) ? Math.abs(minor) % CODENAMES.length : 0;
685
+ return CODENAMES[idx];
653
686
  }
654
687
  function versionLine(version) {
655
688
  return `bt ${version} "${codename(version)}"`;
656
689
  }
657
690
 
691
+ // src/errors.ts
692
+ var EXIT = {
693
+ OK: 0,
694
+ GENERIC: 1,
695
+ DATAERR: 65,
696
+ // EX_DATAERR — bad request / unprocessable / stale cart (400/409/422)
697
+ UNAVAILABLE: 69,
698
+ // EX_UNAVAILABLE — not found (404)
699
+ TEMPFAIL: 75,
700
+ // EX_TEMPFAIL — temporary: 5xx, network/exhausted, rate-limited (429)
701
+ NOPERM: 77
702
+ // EX_NOPERM — auth required / forbidden (401/403)
703
+ };
704
+ function messageOf(e) {
705
+ if (e instanceof Error && e.message) {
706
+ return e.message;
707
+ }
708
+ const s = String(e ?? "").trim();
709
+ return s || "unexpected error";
710
+ }
711
+ function classifyError(e) {
712
+ const message = String(redact(messageOf(e)));
713
+ if (e instanceof RecoverableCartError) {
714
+ return { code: EXIT.DATAERR, type: "stale_cart", status: e.status, message };
715
+ }
716
+ if (e instanceof StoreApiError) {
717
+ const s = e.status;
718
+ if (s === 401 || s === 403) return { code: EXIT.NOPERM, type: "auth", status: s, message };
719
+ if (s === 404) return { code: EXIT.UNAVAILABLE, type: "not_found", status: s, message };
720
+ if (s === 429) return { code: EXIT.TEMPFAIL, type: "rate_limited", status: s, message };
721
+ if (s === 0 || s === 408 || s >= 500) return { code: EXIT.TEMPFAIL, type: "upstream", status: s, message };
722
+ if (s >= 400) return { code: EXIT.DATAERR, type: "client_error", status: s, message };
723
+ }
724
+ return { code: EXIT.GENERIC, type: "error", message };
725
+ }
726
+ function formatFatal(e, json) {
727
+ const info = classifyError(e);
728
+ if (json) {
729
+ const envelope = { error: { code: info.code, type: info.type, message: info.message } };
730
+ return { out: JSON.stringify(envelope, null, 2), err: null, code: info.code };
731
+ }
732
+ const reassurance = "nothing was lost. if a checkout was in flight, your order is safe. file it \u2192 bt report";
733
+ return { out: null, err: `\u2717 ${info.message}
734
+ ${reassurance}`, code: info.code };
735
+ }
736
+
658
737
  // src/brand.ts
659
738
  var ANSI = {
660
739
  reset: "\x1B[0m",
@@ -828,6 +907,53 @@ function resolveLineItemRef(items, ref) {
828
907
  );
829
908
  return byTitle ?? null;
830
909
  }
910
+ function isSubsequence(needle, hay) {
911
+ let i = 0;
912
+ for (let j = 0; j < hay.length && i < needle.length; j++) {
913
+ if (hay[j] === needle[i]) i++;
914
+ }
915
+ return i === needle.length;
916
+ }
917
+ function resolveProductSelector(products, selector) {
918
+ const needle = (selector ?? "").trim();
919
+ if (!needle || products.length === 0) {
920
+ return { candidates: [], fuzzy: false };
921
+ }
922
+ if (/^\d+$/.test(needle)) {
923
+ const idx = Number(needle) - 1;
924
+ if (idx >= 0 && idx < products.length) {
925
+ return { match: products[idx], candidates: [], fuzzy: true };
926
+ }
927
+ return { candidates: [], fuzzy: true };
928
+ }
929
+ const lower = needle.toLowerCase();
930
+ const exact = products.find((p) => (p.handle ?? "").toLowerCase() === lower || p.id === needle);
931
+ if (exact) {
932
+ return { match: exact, candidates: [], fuzzy: false };
933
+ }
934
+ const hits = products.filter(
935
+ (p) => isSubsequence(lower, (p.handle ?? "").toLowerCase()) || isSubsequence(lower, (p.title ?? "").toLowerCase())
936
+ );
937
+ if (hits.length === 1) {
938
+ return { match: hits[0], candidates: [], fuzzy: true };
939
+ }
940
+ return { candidates: hits, fuzzy: true };
941
+ }
942
+ async function resolveProduct(client, region, selector) {
943
+ const needle = (selector ?? "").trim();
944
+ if (needle && !/^\d+$/.test(needle)) {
945
+ const exact = await client.getProduct(needle, { region });
946
+ if (exact) {
947
+ return { product: exact };
948
+ }
949
+ }
950
+ const products = await client.listProducts({ region });
951
+ const r = resolveProductSelector(products, needle);
952
+ if (r.match) {
953
+ return { product: r.match, fuzzy: r.fuzzy };
954
+ }
955
+ return { candidates: r.candidates, fuzzy: r.fuzzy };
956
+ }
831
957
  function resolveOutput(program) {
832
958
  const opts = program.opts();
833
959
  const json = Boolean(opts.json);
@@ -835,6 +961,9 @@ function resolveOutput(program) {
835
961
  return { json, brand };
836
962
  }
837
963
  function printResult(res, json) {
964
+ if (res.exitCode) {
965
+ process.exitCode = res.exitCode;
966
+ }
838
967
  if (json && res.json !== void 0) {
839
968
  process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
840
969
  return;
@@ -912,21 +1041,36 @@ async function defaultRender(url, opts) {
912
1041
  async function runView(client, config, args) {
913
1042
  const style = brandOf(args);
914
1043
  const region = await client.resolveRegion(config.region);
915
- const product = await client.getProduct(args.handle, { region });
1044
+ const picked = await resolveProduct(client, region, args.handle);
1045
+ const product = picked.product;
916
1046
  if (!product) {
1047
+ if (picked.candidates && picked.candidates.length > 1) {
1048
+ return {
1049
+ lines: [
1050
+ fail({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${picked.candidates.length} designs \u2014 pick one:` }),
1051
+ ...picked.candidates.map((p) => ` ${p.handle ?? p.id}`),
1052
+ " or be exact \u2192 bt view <slug>"
1053
+ ],
1054
+ exitCode: EXIT.DATAERR
1055
+ };
1056
+ }
917
1057
  return {
918
1058
  lines: [
919
1059
  fail({
920
- code: 1,
921
- what: `variant ${args.handle} not found.`,
1060
+ code: EXIT.UNAVAILABLE,
1061
+ what: `no design here called "${args.handle}".`,
922
1062
  fix: "bt browse (to see what's in stock)"
923
1063
  })
924
- ]
1064
+ ],
1065
+ exitCode: EXIT.UNAVAILABLE
925
1066
  };
926
1067
  }
927
1068
  const { title, handle } = displayName(product);
928
1069
  const colours = productColours(product);
929
1070
  const lines = [];
1071
+ if (picked.fuzzy && handle !== args.handle.trim()) {
1072
+ lines.push(working(`matched "${handle}" from "${args.handle}"`));
1073
+ }
930
1074
  lines.push(header(style, title));
931
1075
  lines.push(` ${dim(style, handle)}`);
932
1076
  if (product.description) {
@@ -993,7 +1137,7 @@ async function runView(client, config, args) {
993
1137
  };
994
1138
  }
995
1139
  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) => {
1140
+ 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
1141
  const { json, brand } = resolveOutput(program);
998
1142
  const noColor = program.opts().color === false;
999
1143
  const res = await runView(makeClient(), loadConfig(), {
@@ -1011,14 +1155,29 @@ function registerView(program, makeClient, loadConfig) {
1011
1155
  async function runStage(client, config, args) {
1012
1156
  const style = brandOf(args);
1013
1157
  const region = await client.resolveRegion(config.region);
1014
- const product = await client.getProduct(args.handle, { region });
1015
- if (!product) {
1158
+ const resolved = await resolveProduct(client, region, args.handle);
1159
+ if (!resolved.product) {
1160
+ if (resolved.candidates && resolved.candidates.length > 1) {
1161
+ return {
1162
+ lines: [
1163
+ fail({ code: EXIT.DATAERR, what: `"${args.handle}" matches ${resolved.candidates.length} designs \u2014 pick one:` }),
1164
+ ...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`),
1165
+ " or be exact \u2192 bt stage <slug>"
1166
+ ],
1167
+ exitCode: EXIT.DATAERR
1168
+ // ambiguous input → non-zero so `bt stage x && bt checkout` halts (d10)
1169
+ };
1170
+ }
1016
1171
  return {
1017
1172
  lines: [
1018
- fail({ code: 1, what: `variant ${args.handle} not found.`, fix: "bt browse" })
1019
- ]
1173
+ fail({ code: EXIT.UNAVAILABLE, what: `no design here called "${args.handle}".`, fix: "bt browse" })
1174
+ ],
1175
+ exitCode: EXIT.UNAVAILABLE
1176
+ // not-found → non-zero (d10)
1020
1177
  };
1021
1178
  }
1179
+ const product = resolved.product;
1180
+ const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
1022
1181
  const size = args.size ?? config.defaultSize;
1023
1182
  const colours = productColours(product);
1024
1183
  const colour = args.colour ?? config.defaultColour ?? (colours.length > 0 ? colours[0] : void 0);
@@ -1028,17 +1187,20 @@ async function runStage(client, config, args) {
1028
1187
  return {
1029
1188
  lines: [
1030
1189
  fail({
1031
- code: 1,
1190
+ code: EXIT.DATAERR,
1032
1191
  what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
1033
1192
  fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
1034
1193
  })
1035
- ]
1194
+ ],
1195
+ exitCode: EXIT.DATAERR
1196
+ // unprocessable size+colour combo → non-zero (d10)
1036
1197
  };
1037
1198
  }
1038
1199
  if (args.dryRun) {
1039
1200
  const colourSuffix2 = colour ? ` (${colour})` : "";
1040
1201
  return {
1041
1202
  lines: [
1203
+ ...matchNote ? [matchNote] : [],
1042
1204
  ok(`dry-run \u2014 would stage ${product.handle ?? args.handle}${size ? `#${size}` : ""}${colourSuffix2} (nothing staged).`),
1043
1205
  " run without --dry-run to acquire the inventory lock."
1044
1206
  ],
@@ -1054,6 +1216,9 @@ async function runStage(client, config, args) {
1054
1216
  };
1055
1217
  }
1056
1218
  const lines = [];
1219
+ if (matchNote) {
1220
+ lines.push(matchNote);
1221
+ }
1057
1222
  let cartId = config.cart_id;
1058
1223
  let recovered = false;
1059
1224
  if (!cartId) {
@@ -1099,7 +1264,7 @@ async function runStage(client, config, args) {
1099
1264
  };
1100
1265
  }
1101
1266
  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) => {
1267
+ 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
1268
  const { json, brand } = resolveOutput(program);
1104
1269
  const res = await runStage(makeClient(), loadConfig(), {
1105
1270
  handle,
@@ -1134,6 +1299,8 @@ export {
1134
1299
  empty,
1135
1300
  fail,
1136
1301
  versionLine,
1302
+ EXIT,
1303
+ formatFatal,
1137
1304
  cyan,
1138
1305
  dim,
1139
1306
  green,
@@ -1158,4 +1325,4 @@ export {
1158
1325
  runStage,
1159
1326
  registerStage
1160
1327
  };
1161
- //# sourceMappingURL=chunk-7FH7BZ3I.js.map
1328
+ //# sourceMappingURL=chunk-V7YRJ67J.js.map