@binaryandthread/cli 0.1.2 → 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":[]}
@@ -76,6 +76,7 @@ var DEFAULT_RETRY = {
76
76
  maxDelayMs: 2e3
77
77
  };
78
78
  var RETRY_AFTER_MAX_MS = 1e4;
79
+ var DEFAULT_TIMEOUT_MS = 15e3;
79
80
  function parseRetryAfterMs(res) {
80
81
  let raw = null;
81
82
  try {
@@ -140,6 +141,7 @@ var StoreClient = class {
140
141
  logger;
141
142
  retry;
142
143
  sleepImpl;
144
+ timeoutMs;
143
145
  /** Cached resolved regions (read-only catalog optimisation). */
144
146
  regionsCache = null;
145
147
  constructor(opts) {
@@ -159,6 +161,7 @@ var StoreClient = class {
159
161
  this.logger = opts.logger ?? console;
160
162
  this.retry = { ...DEFAULT_RETRY, ...opts.retry ?? {} };
161
163
  this.sleepImpl = opts.sleepImpl ?? sleep;
164
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
162
165
  }
163
166
  // ----- the request engine --------------------------------------------------
164
167
  buildUrl(path, query) {
@@ -201,8 +204,10 @@ var StoreClient = class {
201
204
  let lastErr;
202
205
  for (let attempt = 0; attempt < this.retry.attempts; attempt++) {
203
206
  let retryAfterMs = null;
207
+ const controller = new AbortController();
208
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
204
209
  try {
205
- const res = await this.fetchImpl(url, init);
210
+ const res = await this.fetchImpl(url, { ...init, signal: controller.signal });
206
211
  if (res.ok) {
207
212
  const text = await res.text();
208
213
  return text ? JSON.parse(text) : {};
@@ -250,6 +255,8 @@ var StoreClient = class {
250
255
  }
251
256
  lastErr = e;
252
257
  this.logWarn("store.request.network_retry", { path, attempt: attempt + 1 });
258
+ } finally {
259
+ clearTimeout(timer);
253
260
  }
254
261
  if (attempt < this.retry.attempts - 1) {
255
262
  await this.sleepImpl(retryAfterMs ?? this.backoffMs(attempt));
@@ -666,15 +673,67 @@ var CODENAMES = [
666
673
  "tabs-over-spaces",
667
674
  "vim-not-emacs",
668
675
  "2-space-indent",
669
- "trailing-comma"
676
+ "trailing-comma",
677
+ "rebase-not-merge",
678
+ "lf-not-crlf",
679
+ "semicolons-optional",
680
+ "dark-mode-only"
670
681
  ];
671
- function codename(_version) {
672
- 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];
673
686
  }
674
687
  function versionLine(version) {
675
688
  return `bt ${version} "${codename(version)}"`;
676
689
  }
677
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
+
678
737
  // src/brand.ts
679
738
  var ANSI = {
680
739
  reset: "\x1B[0m",
@@ -848,6 +907,53 @@ function resolveLineItemRef(items, ref) {
848
907
  );
849
908
  return byTitle ?? null;
850
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
+ }
851
957
  function resolveOutput(program) {
852
958
  const opts = program.opts();
853
959
  const json = Boolean(opts.json);
@@ -855,6 +961,9 @@ function resolveOutput(program) {
855
961
  return { json, brand };
856
962
  }
857
963
  function printResult(res, json) {
964
+ if (res.exitCode) {
965
+ process.exitCode = res.exitCode;
966
+ }
858
967
  if (json && res.json !== void 0) {
859
968
  process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
860
969
  return;
@@ -932,21 +1041,36 @@ async function defaultRender(url, opts) {
932
1041
  async function runView(client, config, args) {
933
1042
  const style = brandOf(args);
934
1043
  const region = await client.resolveRegion(config.region);
935
- const product = await client.getProduct(args.handle, { region });
1044
+ const picked = await resolveProduct(client, region, args.handle);
1045
+ const product = picked.product;
936
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
+ }
937
1057
  return {
938
1058
  lines: [
939
1059
  fail({
940
- code: 1,
941
- what: `variant ${args.handle} not found.`,
1060
+ code: EXIT.UNAVAILABLE,
1061
+ what: `no design here called "${args.handle}".`,
942
1062
  fix: "bt browse (to see what's in stock)"
943
1063
  })
944
- ]
1064
+ ],
1065
+ exitCode: EXIT.UNAVAILABLE
945
1066
  };
946
1067
  }
947
1068
  const { title, handle } = displayName(product);
948
1069
  const colours = productColours(product);
949
1070
  const lines = [];
1071
+ if (picked.fuzzy && handle !== args.handle.trim()) {
1072
+ lines.push(working(`matched "${handle}" from "${args.handle}"`));
1073
+ }
950
1074
  lines.push(header(style, title));
951
1075
  lines.push(` ${dim(style, handle)}`);
952
1076
  if (product.description) {
@@ -1013,7 +1137,7 @@ async function runView(client, config, args) {
1013
1137
  };
1014
1138
  }
1015
1139
  function registerView(program, makeClient, loadConfig) {
1016
- 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) => {
1017
1141
  const { json, brand } = resolveOutput(program);
1018
1142
  const noColor = program.opts().color === false;
1019
1143
  const res = await runView(makeClient(), loadConfig(), {
@@ -1031,14 +1155,29 @@ function registerView(program, makeClient, loadConfig) {
1031
1155
  async function runStage(client, config, args) {
1032
1156
  const style = brandOf(args);
1033
1157
  const region = await client.resolveRegion(config.region);
1034
- const product = await client.getProduct(args.handle, { region });
1035
- 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
+ }
1036
1171
  return {
1037
1172
  lines: [
1038
- fail({ code: 1, what: `variant ${args.handle} not found.`, fix: "bt browse" })
1039
- ]
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)
1040
1177
  };
1041
1178
  }
1179
+ const product = resolved.product;
1180
+ const matchNote = resolved.fuzzy && (product.handle ?? "") !== args.handle.trim() ? working(`matched "${product.handle}" from "${args.handle}"`) : null;
1042
1181
  const size = args.size ?? config.defaultSize;
1043
1182
  const colours = productColours(product);
1044
1183
  const colour = args.colour ?? config.defaultColour ?? (colours.length > 0 ? colours[0] : void 0);
@@ -1048,17 +1187,20 @@ async function runStage(client, config, args) {
1048
1187
  return {
1049
1188
  lines: [
1050
1189
  fail({
1051
- code: 1,
1190
+ code: EXIT.DATAERR,
1052
1191
  what: `no variant for ${args.handle}${size ? `#size=${size}` : ""}${colour ? ` (${colour})` : ""}.`,
1053
1192
  fix: `bt view ${args.handle} (to see available sizes + colours)${have}`
1054
1193
  })
1055
- ]
1194
+ ],
1195
+ exitCode: EXIT.DATAERR
1196
+ // unprocessable size+colour combo → non-zero (d10)
1056
1197
  };
1057
1198
  }
1058
1199
  if (args.dryRun) {
1059
1200
  const colourSuffix2 = colour ? ` (${colour})` : "";
1060
1201
  return {
1061
1202
  lines: [
1203
+ ...matchNote ? [matchNote] : [],
1062
1204
  ok(`dry-run \u2014 would stage ${product.handle ?? args.handle}${size ? `#${size}` : ""}${colourSuffix2} (nothing staged).`),
1063
1205
  " run without --dry-run to acquire the inventory lock."
1064
1206
  ],
@@ -1074,6 +1216,9 @@ async function runStage(client, config, args) {
1074
1216
  };
1075
1217
  }
1076
1218
  const lines = [];
1219
+ if (matchNote) {
1220
+ lines.push(matchNote);
1221
+ }
1077
1222
  let cartId = config.cart_id;
1078
1223
  let recovered = false;
1079
1224
  if (!cartId) {
@@ -1119,7 +1264,7 @@ async function runStage(client, config, args) {
1119
1264
  };
1120
1265
  }
1121
1266
  function registerStage(program, makeClient, loadConfig) {
1122
- 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) => {
1123
1268
  const { json, brand } = resolveOutput(program);
1124
1269
  const res = await runStage(makeClient(), loadConfig(), {
1125
1270
  handle,
@@ -1154,6 +1299,8 @@ export {
1154
1299
  empty,
1155
1300
  fail,
1156
1301
  versionLine,
1302
+ EXIT,
1303
+ formatFatal,
1157
1304
  cyan,
1158
1305
  dim,
1159
1306
  green,
@@ -1178,4 +1325,4 @@ export {
1178
1325
  runStage,
1179
1326
  registerStage
1180
1327
  };
1181
- //# sourceMappingURL=chunk-WYCZ6BPV.js.map
1328
+ //# sourceMappingURL=chunk-V7YRJ67J.js.map