@binaryandthread/cli 0.1.0 → 0.1.2

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.
@@ -1,13 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- PLAIN,
4
- colourMarker,
5
- colourSwatch,
6
- cyan,
7
- dim,
8
- header,
9
- makeBrand
10
- } from "./chunk-P3LSUCEO.js";
11
2
 
12
3
  // ../store-client/dist/index.js
13
4
  var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
@@ -53,15 +44,21 @@ function redact(input, _seen) {
53
44
  if (typeof input !== "object") {
54
45
  return input;
55
46
  }
56
- const seen = _seen ?? /* @__PURE__ */ new WeakSet();
57
- if (seen.has(input)) {
58
- return input;
47
+ const seen = _seen ?? /* @__PURE__ */ new Map();
48
+ const existing = seen.get(input);
49
+ if (existing !== void 0) {
50
+ return existing;
59
51
  }
60
- seen.add(input);
61
52
  if (Array.isArray(input)) {
62
- 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;
63
59
  }
64
60
  const out = {};
61
+ seen.set(input, out);
65
62
  for (const [key, value] of Object.entries(input)) {
66
63
  if (SENSITIVE_KEYS.has(key.toLowerCase())) {
67
64
  out[key] = MASK;
@@ -303,10 +300,20 @@ var StoreClient = class {
303
300
  // ----- products (region resolved BEFORE pricing) ---------------------------
304
301
  async listProducts(opts) {
305
302
  const region = opts?.region ?? await this.resolveRegion(opts?.countryCode);
306
- const data = await this.request("GET", "/store/products", {
307
- query: { region_id: region.id, limit: 100 }
308
- });
309
- return data.products ?? [];
303
+ const PAGE = 100;
304
+ const HARD_CAP = 1e3;
305
+ const all = [];
306
+ for (let offset = 0; offset < HARD_CAP; offset += PAGE) {
307
+ const data = await this.request("GET", "/store/products", {
308
+ query: { region_id: region.id, limit: PAGE, offset }
309
+ });
310
+ const page = data.products ?? [];
311
+ all.push(...page);
312
+ if (page.length < PAGE) {
313
+ break;
314
+ }
315
+ }
316
+ return all;
310
317
  }
311
318
  async getProduct(handle, opts) {
312
319
  const region = opts?.region ?? await this.resolveRegion(opts?.countryCode);
@@ -397,7 +404,9 @@ var StoreClient = class {
397
404
  return res.cart;
398
405
  }
399
406
  // ----- headless contract (04.5-01) -----------------------------------------
400
- async mintCheckout(cartId, email, channel = "cli") {
407
+ // `channel` is REQUIRED (L11): a default silently mis-attributes analytics for a future caller that
408
+ // forgets it. Both current callers (cli, mcp) pass it explicitly.
409
+ async mintCheckout(cartId, email, channel) {
401
410
  return this.request("POST", "/store/headless/checkout", {
402
411
  idempotent: true,
403
412
  body: { cart_id: cartId, email, channel }
@@ -470,13 +479,15 @@ var StoreClient = class {
470
479
  }
471
480
  }
472
481
  /**
473
- * Guest order lookup — requires order display id + EXACT email (F-H1).
474
- * The backend returns 404 on a mismatch (no enumeration); the client surfaces null.
482
+ * Guest order lookup (AUD-8) — POST /store/order-lookup with the order id (display id OR internal
483
+ * id) + the EXACT email in the BODY (never the query string, so the email is not access-logged).
484
+ * The backend returns 404 on any mismatch (no enumeration) → the client surfaces null. Rate-limited
485
+ * server-side; returns minimal status only, never PII (F-H1 / F-H3).
475
486
  */
476
- async getOrder(displayId, email) {
487
+ async getOrder(idOrDisplayId, email) {
477
488
  try {
478
- return await this.request("GET", "/store/orders", {
479
- query: { display_id: String(displayId), email }
489
+ return await this.request("POST", "/store/order-lookup", {
490
+ body: { id: String(idOrDisplayId), email }
480
491
  });
481
492
  } catch (e) {
482
493
  if (e instanceof StoreApiError && e.status === 404) {
@@ -576,7 +587,7 @@ function findVariant(p, opts = {}) {
576
587
  return variants.find((v) => colourMatches(v));
577
588
  }
578
589
  if (size && !colour) {
579
- return variants.find((v) => sizeMatches(v)) ?? variants[0];
590
+ return variants.find((v) => sizeMatches(v));
580
591
  }
581
592
  return variants[0];
582
593
  }
@@ -664,6 +675,104 @@ function versionLine(version) {
664
675
  return `bt ${version} "${codename(version)}"`;
665
676
  }
666
677
 
678
+ // src/brand.ts
679
+ var ANSI = {
680
+ reset: "\x1B[0m",
681
+ // cyan: the signature accent. On a dark terminal this is the neon "glow" the design system
682
+ // reserves for key elements. 38;5;48 ≈ #00ffc2 family on 256-colour terminals.
683
+ cyan: "\x1B[38;5;48m",
684
+ // green: reserved for the contribution graph ONLY — the GitHub-contribution-green flex on the
685
+ // `bt whoami` card. Not a general accent (that's cyan); a deliberate, single second colour.
686
+ green: "\x1B[38;5;42m",
687
+ dim: "\x1B[2m",
688
+ bold: "\x1B[1m"
689
+ };
690
+ function colorEnabled(opts = {}) {
691
+ if (opts.noColor) return false;
692
+ if (process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") return false;
693
+ const tty = opts.isTTY ?? Boolean(process.stdout.isTTY);
694
+ return tty;
695
+ }
696
+ function makeBrand(opts = {}) {
697
+ return { color: colorEnabled(opts) };
698
+ }
699
+ var PLAIN = { color: false };
700
+ function wrap(style, code, s) {
701
+ if (!style.color) return s;
702
+ return `${code}${s}${ANSI.reset}`;
703
+ }
704
+ function cyan(style, s) {
705
+ return wrap(style, ANSI.cyan, s);
706
+ }
707
+ function dim(style, s) {
708
+ return wrap(style, ANSI.dim, s);
709
+ }
710
+ function green(style, s) {
711
+ return wrap(style, ANSI.green, s);
712
+ }
713
+ var BOX = {
714
+ tl: "\u250C",
715
+ tr: "\u2510",
716
+ bl: "\u2514",
717
+ br: "\u2518",
718
+ h: "\u2500",
719
+ v: "\u2502",
720
+ ml: "\u251C",
721
+ mr: "\u2524"
722
+ };
723
+ function header(style, title, width = 56) {
724
+ const label = ` ${title} `;
725
+ const lead = BOX.h.repeat(2);
726
+ const used = lead.length + label.length;
727
+ const tail = BOX.h.repeat(Math.max(2, width - used));
728
+ return `${lead}${cyan(style, label)}${tail}`;
729
+ }
730
+ function rule(width = 56) {
731
+ return BOX.h.repeat(width);
732
+ }
733
+ function box(lines, opts = {}) {
734
+ const style = opts.style ?? PLAIN;
735
+ const inner = opts.width ?? 56;
736
+ const out = [];
737
+ if (opts.title) {
738
+ const label = ` ${opts.title} `;
739
+ const lead = BOX.h.repeat(2);
740
+ const used = lead.length + visibleLength(label);
741
+ const tail = BOX.h.repeat(Math.max(1, inner - used));
742
+ out.push(`${BOX.tl}${lead}${cyan(style, label)}${tail}${BOX.tr}`);
743
+ } else {
744
+ out.push(`${BOX.tl}${BOX.h.repeat(inner)}${BOX.tr}`);
745
+ }
746
+ for (const line of lines) {
747
+ const pad = Math.max(0, inner - 1 - visibleLength(line));
748
+ out.push(`${BOX.v} ${line}${" ".repeat(pad)}${BOX.v}`);
749
+ }
750
+ out.push(`${BOX.bl}${BOX.h.repeat(inner)}${BOX.br}`);
751
+ return out;
752
+ }
753
+ function visibleLength(s) {
754
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
755
+ }
756
+ function contributionBar(style, commits, cap = 20) {
757
+ const n = Number.isFinite(commits) ? Math.max(0, Math.floor(commits)) : 0;
758
+ const filledCount = Math.min(n, cap);
759
+ const filled = green(style, "\u2587".repeat(filledCount));
760
+ const empty2 = "\u2591".repeat(Math.max(0, cap - filledCount));
761
+ return `${filled}${empty2}`;
762
+ }
763
+ function colourSwatch(colour) {
764
+ const c = colour.trim().toLowerCase();
765
+ if (c === "white") return "\u2B1C";
766
+ if (c === "black") return "\u2B1B";
767
+ return "\u25FC";
768
+ }
769
+ function colourMarker(colours) {
770
+ if (colours.length === 0) return "";
771
+ const swatches = colours.map((c) => colourSwatch(c)).join("");
772
+ const names = colours.join("/");
773
+ return `${swatches} ${names}`;
774
+ }
775
+
667
776
  // src/commands/shared.ts
668
777
  function brandOf(args) {
669
778
  return args?.brand ?? PLAIN;
@@ -739,15 +848,35 @@ function resolveLineItemRef(items, ref) {
739
848
  );
740
849
  return byTitle ?? null;
741
850
  }
851
+ function resolveOutput(program) {
852
+ const opts = program.opts();
853
+ const json = Boolean(opts.json);
854
+ const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor: opts.color === false });
855
+ return { json, brand };
856
+ }
857
+ function printResult(res, json) {
858
+ if (json && res.json !== void 0) {
859
+ process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
860
+ return;
861
+ }
862
+ process.stdout.write(res.lines.join("\n") + "\n");
863
+ }
742
864
 
743
865
  // src/image-ladder.ts
744
866
  var DEFAULT_RENDER_HEIGHT = 28;
745
867
  function urlFloor(url) {
746
868
  return ` ${url}`;
747
869
  }
870
+ function isHttpsUrl(url) {
871
+ try {
872
+ return new URL(url).protocol === "https:";
873
+ } catch {
874
+ return false;
875
+ }
876
+ }
748
877
  async function renderImage(blobUrl, opts = {}) {
749
878
  const noColor = opts.noColor ?? Boolean(process.env.NO_COLOR);
750
- if (!isAllowedImageHost(blobUrl)) {
879
+ if (!isAllowedImageHost(blobUrl) || !isHttpsUrl(blobUrl)) {
751
880
  return ` (image host not allowed \u2014 showing link only)
752
881
  ${urlFloor(blobUrl)}`;
753
882
  }
@@ -764,6 +893,7 @@ ${urlFloor(blobUrl)}`;
764
893
  }
765
894
  }
766
895
  var IMAGE_FETCH_TIMEOUT_MS = 5e3;
896
+ var IMAGE_BYTE_CAP = 2 * 1024 * 1024;
767
897
  async function defaultRender(url, opts) {
768
898
  const mod = await import("terminal-image");
769
899
  const terminalImage = mod.default ?? mod;
@@ -782,7 +912,15 @@ async function defaultRender(url, opts) {
782
912
  if (!res.ok) {
783
913
  throw new Error(`blob fetch failed: HTTP ${res.status}`);
784
914
  }
915
+ const cap = opts.byteCap ?? IMAGE_BYTE_CAP;
916
+ const declaredLen = Number.parseInt(res.headers?.get?.("content-length") ?? "", 10);
917
+ if (Number.isFinite(declaredLen) && declaredLen > cap) {
918
+ throw new Error("blob exceeds byte cap (declared)");
919
+ }
785
920
  const buf = Buffer.from(await res.arrayBuffer());
921
+ if (buf.byteLength === 0 || buf.byteLength > cap) {
922
+ throw new Error("blob empty or exceeds byte cap");
923
+ }
786
924
  return bufferFn(buf, {
787
925
  height: opts.height ?? DEFAULT_RENDER_HEIGHT,
788
926
  preserveAspectRatio: true,
@@ -790,94 +928,6 @@ async function defaultRender(url, opts) {
790
928
  });
791
929
  }
792
930
 
793
- // src/commands/browse.ts
794
- function productRow(p, stage, style) {
795
- const { amount, currency } = firstPrice(p);
796
- const { title, handle } = displayName(p);
797
- const colours = colourMarker(productColours(p));
798
- const parts = [
799
- title.padEnd(22),
800
- cyan(style, fmtPrice(amount, currency).padEnd(11)),
801
- dim(style, handle.padEnd(34)),
802
- stageMarker(stage)
803
- ];
804
- const row = parts.join(" ").trimEnd();
805
- return colours ? `${row}
806
- ${dim(style, colours)}` : row;
807
- }
808
- async function runBrowse(client, config, args) {
809
- const style = brandOf(args);
810
- const region = await client.resolveRegion(config.region);
811
- const products = await client.listProducts({ region });
812
- const rows = products.map((p) => ({
813
- p,
814
- stage: p.metadata?.["channel_stage"] ?? "main"
815
- }));
816
- let drops = [];
817
- if (args.branch === "next") {
818
- drops = await client.listDrops();
819
- }
820
- const lines = [];
821
- if (rows.length === 0 && drops.length === 0) {
822
- lines.push(empty("nothing in stock on this branch yet. main is the latest."));
823
- lines.push(" peek at next \u2192 bt browse --branch=next");
824
- } else {
825
- lines.push(header(style, "bt browse \u2014 in stock"));
826
- for (const { p, stage } of rows) {
827
- lines.push(productRow(p, stage, style));
828
- }
829
- for (const d of drops) {
830
- const dt = (d.title ?? d.handle ?? d.id).toString();
831
- lines.push(
832
- [
833
- dt.padEnd(22),
834
- cyan(style, fmtPrice(d.price, region.currency_code).padEnd(11)),
835
- dim(style, (d.handle ?? d.id).padEnd(34)),
836
- stageMarker(d.channel_stage)
837
- ].join(" ").trimEnd()
838
- );
839
- }
840
- if (drops.length > 0) {
841
- lines.push(" on next, before the web \u2192 bt drops");
842
- }
843
- lines.push(" view one \u2192 bt view <handle> \xB7 stage one \u2192 bt stage <handle> --size L");
844
- }
845
- return {
846
- lines,
847
- json: args.json ? products.map((p) => ({
848
- id: p.id,
849
- handle: p.handle,
850
- title: p.title,
851
- price: firstPrice(p).amount,
852
- currency: firstPrice(p).currency,
853
- colours: productColours(p),
854
- channel_stage: p.metadata?.["channel_stage"] ?? "main"
855
- })) : void 0
856
- };
857
- }
858
- function registerBrowse(program, makeClient, loadConfig) {
859
- program.command("browse").alias("ls").description("list what's in stock on a branch.").option("--branch <branch>", "main (live) or next (terminal-only drops)").option("--plain", "force the static table (skip the interactive picker)").action(async (opts) => {
860
- const gOpts = program.opts();
861
- const json = Boolean(gOpts.json);
862
- const noColor = gOpts.color === false;
863
- const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor });
864
- if (!json && !opts.plain && brand.color && process.stdout.isTTY) {
865
- const { runInteractiveBrowse } = await import("./interactive-TFZOY3JU.js");
866
- const handled = await runInteractiveBrowse(makeClient(), loadConfig(), { branch: opts.branch, brand });
867
- if (handled) return;
868
- }
869
- const res = await runBrowse(makeClient(), loadConfig(), { branch: opts.branch, json, brand });
870
- printResult(res, json);
871
- });
872
- }
873
- function printResult(res, json) {
874
- if (json && res.json !== void 0) {
875
- process.stdout.write(JSON.stringify(res.json, null, 2) + "\n");
876
- return;
877
- }
878
- process.stdout.write(res.lines.join("\n") + "\n");
879
- }
880
-
881
931
  // src/commands/view.ts
882
932
  async function runView(client, config, args) {
883
933
  const style = brandOf(args);
@@ -964,10 +1014,8 @@ async function runView(client, config, args) {
964
1014
  }
965
1015
  function registerView(program, makeClient, loadConfig) {
966
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) => {
967
- const gOpts = program.opts();
968
- const json = Boolean(gOpts.json);
969
- const noColor = gOpts.color === false;
970
- const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor });
1017
+ const { json, brand } = resolveOutput(program);
1018
+ const noColor = program.opts().color === false;
971
1019
  const res = await runView(makeClient(), loadConfig(), {
972
1020
  handle,
973
1021
  colour: opts.colour,
@@ -1072,9 +1120,7 @@ async function runStage(client, config, args) {
1072
1120
  }
1073
1121
  function registerStage(program, makeClient, loadConfig) {
1074
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) => {
1075
- const gOpts = program.opts();
1076
- const json = Boolean(gOpts.json);
1077
- const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor: gOpts.color === false });
1123
+ const { json, brand } = resolveOutput(program);
1078
1124
  const res = await runStage(makeClient(), loadConfig(), {
1079
1125
  handle,
1080
1126
  size: opts.size,
@@ -1108,6 +1154,15 @@ export {
1108
1154
  empty,
1109
1155
  fail,
1110
1156
  versionLine,
1157
+ cyan,
1158
+ dim,
1159
+ green,
1160
+ header,
1161
+ rule,
1162
+ box,
1163
+ contributionBar,
1164
+ colourSwatch,
1165
+ colourMarker,
1111
1166
  brandOf,
1112
1167
  fmtPrice,
1113
1168
  firstPrice,
@@ -1116,11 +1171,11 @@ export {
1116
1171
  lineColour,
1117
1172
  formatCartLine,
1118
1173
  resolveLineItemRef,
1174
+ resolveOutput,
1175
+ printResult,
1119
1176
  runView,
1120
1177
  registerView,
1121
1178
  runStage,
1122
- registerStage,
1123
- registerBrowse,
1124
- printResult
1179
+ registerStage
1125
1180
  };
1126
- //# sourceMappingURL=chunk-DWTH7CNI.js.map
1181
+ //# sourceMappingURL=chunk-WYCZ6BPV.js.map