@binaryandthread/cli 0.2.0 → 0.3.0

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.
@@ -622,9 +622,24 @@ function findVariant(p, opts = {}) {
622
622
  }
623
623
  return variants[0];
624
624
  }
625
+ var PREFERRED_SCENE = "plp";
626
+ function sceneImage(images, scene, colour) {
627
+ for (const img of images ?? []) {
628
+ const u = (img?.url ?? "").trim();
629
+ if (!u || !u.toLowerCase().includes(`-${scene}-`)) continue;
630
+ if (colour === null || new RegExp(`(^|[^a-z])${escapeRegex(colour)}([^a-z]|$)`, "i").test(u.toLowerCase())) {
631
+ return u;
632
+ }
633
+ }
634
+ return null;
635
+ }
625
636
  function imageForColour(p, colour) {
626
637
  const target = colour.trim().toLowerCase();
627
- if (!target) return p.thumbnail ?? firstImageUrl(p.images) ?? null;
638
+ if (!target) {
639
+ return sceneImage(p.images, PREFERRED_SCENE, null) ?? p.thumbnail ?? firstImageUrl(p.images) ?? null;
640
+ }
641
+ const plp = sceneImage(p.images, PREFERRED_SCENE, target);
642
+ if (plp) return plp;
628
643
  for (const v of p.variants ?? []) {
629
644
  if (variantColour(v) !== target) continue;
630
645
  const url = firstImageUrl(v.images);
@@ -1094,6 +1109,14 @@ ${urlFloor(blobUrl)}`;
1094
1109
  }
1095
1110
  var IMAGE_FETCH_TIMEOUT_MS = 5e3;
1096
1111
  var IMAGE_BYTE_CAP = 2 * 1024 * 1024;
1112
+ function isWebp(buf) {
1113
+ return buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP";
1114
+ }
1115
+ async function toPng(buf) {
1116
+ if (!isWebp(buf)) return buf;
1117
+ const sharp = (await import("sharp")).default;
1118
+ return sharp(buf).png().toBuffer();
1119
+ }
1097
1120
  async function defaultRender(url, opts) {
1098
1121
  const mod = await import("terminal-image");
1099
1122
  const terminalImage = mod.default ?? mod;
@@ -1117,10 +1140,11 @@ async function defaultRender(url, opts) {
1117
1140
  if (Number.isFinite(declaredLen) && declaredLen > cap) {
1118
1141
  throw new Error("blob exceeds byte cap (declared)");
1119
1142
  }
1120
- const buf = Buffer.from(await res.arrayBuffer());
1121
- if (buf.byteLength === 0 || buf.byteLength > cap) {
1143
+ const rawBuf = Buffer.from(await res.arrayBuffer());
1144
+ if (rawBuf.byteLength === 0 || rawBuf.byteLength > cap) {
1122
1145
  throw new Error("blob empty or exceeds byte cap");
1123
1146
  }
1147
+ const buf = await toPng(rawBuf);
1124
1148
  return bufferFn(buf, {
1125
1149
  height: opts.height ?? DEFAULT_RENDER_HEIGHT,
1126
1150
  preserveAspectRatio: true,
@@ -1410,4 +1434,4 @@ export {
1410
1434
  runStage,
1411
1435
  registerStage
1412
1436
  };
1413
- //# sourceMappingURL=chunk-NJ3DW5AX.js.map
1437
+ //# sourceMappingURL=chunk-RDJJLYXB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../store-client/src/redact.ts","../../store-client/src/client.ts","../../store-client/src/defaults.ts","../../store-client/src/host-allowlist.ts","../../store-client/src/colour.ts","../../store-client/src/product.ts","../../store-client/src/tax.ts","../src/voice.ts","../src/errors.ts","../src/brand.ts","../src/commands/shared.ts","../src/image-ladder.ts","../src/commands/view.ts","../src/commands/stage.ts"],"sourcesContent":["// Secret/PII redaction for logs and error envelopes (SECURITY G10).\n//\n// The single rule for the whole channel: Authorization headers, tokens, and full\n// emails NEVER reach a log line, an error message, or an MCP/CLI transcript. This\n// function is applied by the StoreClient's logger and error mapper before anything\n// is emitted. It is consumed by BOTH the CLI and (Wave 4) the MCP — one redactor.\n\nconst SENSITIVE_KEYS = new Set([\n \"authorization\",\n \"token\",\n \"access_token\",\n \"refresh_token\",\n \"jwt\",\n \"idempotency-key\",\n \"x-publishable-api-key\",\n \"password\",\n \"secret\",\n \"cookie\",\n \"set-cookie\",\n])\n\nconst MASK = \"[REDACTED]\"\n\n// Bearer-token-in-a-string (e.g. \"Authorization: Bearer abc.def.ghi\" or a stray \"Bearer xyz\").\nconst BEARER_RE = /Bearer\\s+[A-Za-z0-9._\\-]+/gi\n// A plausible email anywhere in a string.\nconst EMAIL_RE = /([^\\s@]+)@([^\\s@]+\\.[^\\s@]+)/g\n\n/** Mask an email to `d***@x` form (first char + stars, host first char). */\nexport function maskEmail(email: string): string {\n const at = email.indexOf(\"@\")\n if (at <= 0) {\n return email\n }\n const local = email.slice(0, at)\n const host = email.slice(at + 1)\n const localMasked = local[0] + \"*\".repeat(Math.max(2, local.length - 1))\n const hostMasked = host.length > 0 ? host[0] : \"x\"\n return `${localMasked}@${hostMasked}`\n}\n\nfunction redactString(s: string): string {\n return s.replace(BEARER_RE, \"Bearer [REDACTED]\").replace(EMAIL_RE, (_m, _l, _h) =>\n maskEmail(`${_l}@${_h}`)\n )\n}\n\n/**\n * Deep-redact an object/array/string. Returns a structurally-similar value with\n * sensitive keys masked, Bearer tokens stripped, and emails masked. Never throws.\n *\n * `_seen` memoises input-object → its REDACTED output. This does double duty: it makes the\n * function cycle-safe (a revisited object resolves to its own — possibly still-filling — output\n * container) AND ensures a REPEATED reference is redacted every time. The previous WeakSet guard\n * returned the *original* object on revisit, leaking any token/email reachable via a 2nd path (G10).\n * The output container is registered BEFORE recursing so a cycle points at the redacted copy.\n */\nexport function redact<T>(input: T, _seen?: Map<object, unknown>): T {\n if (input == null) {\n return input\n }\n if (typeof input === \"string\") {\n return redactString(input) as unknown as T\n }\n if (typeof input !== \"object\") {\n return input\n }\n\n const seen = _seen ?? new Map<object, unknown>()\n const existing = seen.get(input as object)\n if (existing !== undefined) {\n return existing as T\n }\n\n if (Array.isArray(input)) {\n const out: unknown[] = []\n seen.set(input as object, out) // register before recursing (cycle-safe)\n for (const v of input) {\n out.push(redact(v, seen))\n }\n return out as unknown as T\n }\n\n const out: Record<string, unknown> = {}\n seen.set(input as object, out) // register before recursing (cycle-safe)\n for (const [key, value] of Object.entries(input as Record<string, unknown>)) {\n if (SENSITIVE_KEYS.has(key.toLowerCase())) {\n out[key] = MASK\n } else if (key.toLowerCase() === \"email\" && typeof value === \"string\") {\n out[key] = maskEmail(value)\n } else {\n out[key] = redact(value, seen)\n }\n }\n return out as unknown as T\n}\n","// StoreClient — the ONE shared HTTP client over the B&T Medusa Store API.\n//\n// Design (ROBUSTNESS §0): the clients are DUMB. Every dangerous property lives in the\n// backend. This module is the single place the channel's HTTP-client invariants live so\n// they are written once and contract-tested once (the CLI and the Wave-4 MCP both use it):\n//\n// - publishable key on EVERY request (public by design, T6); customer JWT when present\n// - Idempotency-Key (crypto.randomUUID) on EVERY mutation, STABLE across a retry — honoured by\n// the checkout MINT (per-cart session reuse); the core cart/line-item routes do NOT yet honour\n// it, so that guarantee needs a backend idempotency middleware (BACKLOG) (§2)\n// - retry 5xx + network errors with jittered backoff; NEVER retry a 4xx (F-A1/F-I2)\n// - region resolved before pricing — never a bare currency literal (F-A2/INV-11)\n// - 404/409/cart.completed on a cart mutation → RecoverableCartError (CLI makes a fresh cart) (F-B2)\n// - all logs + error envelopes pass through redact() — no token/email ever leaks (G10)\n// - NO secret key is ever stored or sent — publishable key only (T6)\n\nimport { redact } from \"./redact\"\nimport type {\n CartLike,\n ClientLogger,\n DropRow,\n HeadlessCheckout,\n OrderSummary,\n PollResult,\n ProductLike,\n RegionLike,\n RegionResolution,\n ReportResult,\n RequestLinkResult,\n RetryPolicy,\n ShippingOptionLike,\n StoreClientOptions,\n VerifyLinkResult,\n WhoAmI,\n} from \"./types\"\n\nconst DEFAULT_RETRY: RetryPolicy = {\n attempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 2000,\n}\n\n/** Cap on how long we'll honor a server's Retry-After, so a hostile/huge value can't hang us. */\nconst RETRY_AFTER_MAX_MS = 10_000\n\n/**\n * Per-attempt request timeout. A black-holed connection (socket accepted, no response) would\n * otherwise never settle, hanging every command/tool AND defeating the checkout poll's 20-min\n * deadline (F-I3) — the deadline can't be re-checked while stuck awaiting a dead fetch. 15s is\n * generous for the Store API while still failing closed on a hang.\n */\nconst DEFAULT_TIMEOUT_MS = 15_000\n\n/**\n * Extra fields to request on product reads. `+metadata` ADDS metadata to Medusa's default field set\n * (it doesn't replace it — variants + calculated_price still come through). metadata is where lore /\n * lore_sources / per-product spec fields live, and the Store API omits it unless asked. The `+` is\n * URL-encoded to %2B by URLSearchParams, which Medusa decodes back to `+metadata`.\n */\nconst PRODUCT_FIELDS = \"+metadata\"\n\n/**\n * Parse an HTTP Retry-After header → ms, or null if absent/unparseable. Accepts both forms:\n * delta-seconds (\"2\") and an HTTP-date. Capped at RETRY_AFTER_MAX_MS (d12).\n */\nfunction parseRetryAfterMs(res: Response): number | null {\n let raw: string | null = null\n try {\n raw = res.headers?.get?.(\"retry-after\") ?? null\n } catch {\n return null\n }\n if (!raw) {\n return null\n }\n const secs = Number(raw)\n let ms: number\n if (Number.isFinite(secs)) {\n ms = secs * 1000\n } else {\n const dateMs = Date.parse(raw)\n if (!Number.isFinite(dateMs)) {\n return null\n }\n ms = dateMs - Date.now()\n }\n return Math.min(Math.max(0, ms), RETRY_AFTER_MAX_MS)\n}\n\n/** A cart error the CLI maps to \"discard local cart_id, create a fresh cart, re-add\" (F-B2). */\nexport class RecoverableCartError extends Error {\n readonly recoverable = true as const\n readonly status: number\n constructor(message: string, status: number) {\n super(message)\n this.name = \"RecoverableCartError\"\n this.status = status\n }\n}\n\n/** A non-recoverable API error. Its message/envelope is already redacted (G10). */\nexport class StoreApiError extends Error {\n readonly status: number\n readonly path: string\n constructor(message: string, status: number, path: string) {\n super(message)\n this.name = \"StoreApiError\"\n this.status = status\n this.path = path\n }\n}\n\ntype HttpMethod = \"GET\" | \"POST\" | \"DELETE\"\n\ninterface RequestOpts {\n body?: unknown\n /** Mutations carry an Idempotency-Key. */\n idempotent?: boolean\n /** Treat a 404/409/cart.completed as a RecoverableCartError (cart mutations). */\n cartRecovery?: boolean\n query?: Record<string, string | number | undefined>\n /**\n * Statuses for which a 4xx is EXPECTED, not an error — the client_error envelope is NOT\n * logged (BUG 3: a guest whoami 401 is the normal unauthenticated path, not noise to dump).\n * The error is still thrown so the caller can branch on it.\n */\n expected4xx?: number[]\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** crypto.randomUUID with a defensive fallback (older runtimes). */\nfunction newUuid(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) {\n return g.crypto.randomUUID()\n }\n // RFC4122-ish fallback — never the happy path, just defensive.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nexport class StoreClient {\n private readonly baseUrl: string\n private readonly publishableKey: string\n private readonly jwt?: string\n private readonly fetchImpl: typeof fetch\n private readonly logger: ClientLogger\n private readonly retry: RetryPolicy\n private readonly sleepImpl: (ms: number) => Promise<void>\n private readonly timeoutMs: number\n\n /** Cached resolved regions (read-only catalog optimisation). */\n private regionsCache: RegionLike[] | null = null\n\n constructor(opts: StoreClientOptions) {\n if (!opts.baseUrl) {\n throw new Error(\"StoreClient: baseUrl is required\")\n }\n if (!opts.publishableKey) {\n throw new Error(\"StoreClient: publishableKey is required\")\n }\n // Guard against accidentally shipping a secret key (T6 — defensive).\n if (/^sk_/.test(opts.publishableKey)) {\n throw new Error(\"StoreClient: refusing a secret key — the client carries only the publishable key (T6)\")\n }\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\")\n this.publishableKey = opts.publishableKey\n this.jwt = opts.jwt\n this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch as typeof fetch)\n this.logger = opts.logger ?? console\n this.retry = { ...DEFAULT_RETRY, ...(opts.retry ?? {}) }\n this.sleepImpl = opts.sleepImpl ?? sleep\n this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n }\n\n // ----- the request engine --------------------------------------------------\n\n private buildUrl(path: string, query?: RequestOpts[\"query\"]): string {\n const url = new URL(this.baseUrl + path)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== null && v !== \"\") {\n url.searchParams.set(k, String(v))\n }\n }\n }\n return url.toString()\n }\n\n private baseHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-publishable-api-key\": this.publishableKey,\n }\n if (this.jwt) {\n headers[\"Authorization\"] = `Bearer ${this.jwt}`\n }\n return headers\n }\n\n private backoffMs(attemptIndex: number): number {\n // exponential base with full jitter, capped.\n const exp = this.retry.baseDelayMs * 2 ** attemptIndex\n const capped = Math.min(exp, this.retry.maxDelayMs)\n return Math.floor(Math.random() * (capped + 1))\n }\n\n private async request<T>(method: HttpMethod, path: string, opts: RequestOpts = {}): Promise<T> {\n const headers = this.baseHeaders()\n // Generate the idempotency key ONCE and reuse it across retries (ROBUSTNESS §2). This only\n // prevents a duplicate when the SERVER honours Idempotency-Key: the checkout MINT does (it\n // reuses the open per-cart session), but the core Medusa cart/line-item routes do NOT yet — so a\n // retried createCart/addLineItem after a committed-but-lost response can still double-apply. The\n // real fix is a backend idempotency middleware (BACKLOG); until then a double-add is at least\n // VISIBLE (the CLI review + MCP checkout total show the real cart before any charge).\n if (opts.idempotent) {\n headers[\"Idempotency-Key\"] = newUuid()\n }\n const url = this.buildUrl(path, opts.query)\n const init: RequestInit = {\n method,\n headers,\n ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),\n }\n\n let lastErr: unknown\n for (let attempt = 0; attempt < this.retry.attempts; attempt++) {\n // A server-dictated delay for THIS attempt's backoff (429 Retry-After); null → jittered backoff.\n let retryAfterMs: number | null = null\n // Bound THIS attempt: a hung connection is aborted after timeoutMs. The AbortError lands in\n // the catch below and is treated as a retryable network error (so the backoff + retry, and\n // ultimately the exhausted-retries StoreApiError, all apply). Fresh controller per attempt.\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n try {\n const res = await this.fetchImpl(url as unknown as string, { ...init, signal: controller.signal })\n if (res.ok) {\n // 204 / empty body tolerance\n const text = await res.text()\n return (text ? JSON.parse(text) : {}) as T\n }\n\n // Read the error body defensively (may not be JSON).\n let parsed: Record<string, unknown> = {}\n try {\n const t = await res.text()\n parsed = t ? (JSON.parse(t) as Record<string, unknown>) : {}\n } catch {\n parsed = {}\n }\n\n // Cart recovery: a stale/completed cart on a cart mutation (F-B2).\n if (\n opts.cartRecovery &&\n (res.status === 404 ||\n res.status === 409 ||\n String(parsed.type ?? \"\").includes(\"cart.completed\") ||\n /cart.*complete/i.test(String(parsed.message ?? \"\")))\n ) {\n throw new RecoverableCartError(\n `cart is stale or completed (HTTP ${res.status})`,\n res.status\n )\n }\n\n // 429 Too Many Requests — RETRYABLE, honoring Retry-After (d12). Must be handled BEFORE\n // the generic 4xx-no-retry rule below, or a rate limit becomes a hard failure.\n if (res.status === 429) {\n retryAfterMs = parseRetryAfterMs(res)\n lastErr = new StoreApiError(`rate limited (HTTP 429)`, 429, path)\n this.logWarn(\"store.request.rate_limited\", {\n path,\n attempt: attempt + 1,\n retryAfterMs,\n })\n } else if (res.status >= 400 && res.status < 500) {\n // other 4xx is a client error — do NOT retry.\n // Suppress the envelope for EXPECTED 4xx (e.g. a guest whoami 401 — BUG 3): it's a\n // normal branch the caller handles, not noise to dump to the user's terminal.\n if (!(opts.expected4xx ?? []).includes(res.status)) {\n this.logError(\"store.request.client_error\", { path, status: res.status, body: parsed })\n }\n throw new StoreApiError(\n `request failed (HTTP ${res.status})`,\n res.status,\n path\n )\n } else {\n // 5xx — retryable. Fall through to the retry loop.\n lastErr = new StoreApiError(`upstream error (HTTP ${res.status})`, res.status, path)\n this.logWarn(\"store.request.retryable\", { path, status: res.status, attempt: attempt + 1 })\n }\n } catch (e) {\n // RecoverableCartError / StoreApiError(4xx) must propagate immediately.\n if (e instanceof RecoverableCartError) {\n throw e\n }\n if (e instanceof StoreApiError && e.status >= 400 && e.status < 500) {\n throw e\n }\n // Network error, an aborted (timed-out) request, or a 5xx StoreApiError → retryable.\n lastErr = e\n this.logWarn(\"store.request.network_retry\", { path, attempt: attempt + 1 })\n } finally {\n // Always clear the per-attempt timeout — on success-return, on a rethrow, or before a retry.\n clearTimeout(timer)\n }\n\n // Backoff before the next attempt (skip after the final attempt). A 429 Retry-After, when\n // present, dictates the delay; otherwise jittered exponential backoff (d12).\n if (attempt < this.retry.attempts - 1) {\n await this.sleepImpl(retryAfterMs ?? this.backoffMs(attempt))\n }\n }\n\n // Exhausted retries. The thrown envelope is already free of secrets (we only ever\n // log/throw redacted, derived data — never the request headers/body).\n this.logError(\"store.request.exhausted\", { path, attempts: this.retry.attempts })\n if (lastErr instanceof StoreApiError) {\n throw lastErr\n }\n throw new StoreApiError(\n `request failed after ${this.retry.attempts} attempts: ${path}`,\n 0,\n path\n )\n }\n\n private logWarn(event: string, ctx: Record<string, unknown>): void {\n this.logger.warn(event, redact(ctx))\n }\n\n private logError(event: string, ctx: Record<string, unknown>): void {\n this.logger.error(event, redact(ctx))\n }\n\n // ----- regions (F-A2 / INV-11) ---------------------------------------------\n\n async getRegions(): Promise<RegionLike[]> {\n const data = await this.request<{ regions: RegionLike[] }>(\"GET\", \"/store/regions\")\n this.regionsCache = data.regions ?? []\n return this.regionsCache\n }\n\n /**\n * Resolve a region by country code, WITH provenance (matched / home / requested), so callers on\n * the money path can fail closed on an unmatched explicit country instead of silently pricing in\n * the wrong currency (F-A2). Region #1 is the launch/home default — SEK today, derived, never a\n * hardcoded literal.\n *\n * Semantics:\n * - country given AND a region ships there → { matched: true, home: false }.\n * - country given, none ships there → { matched: false, home: false } (money surfaces MUST\n * refuse; discovery may show the home region + a warning). We still return a region so a\n * discovery command can render *something*, but `matched:false` is the signal to fail closed.\n * - no country given → { matched: false, home: true } (the legitimate\n * launch default — not a \"wrong\" currency).\n */\n async resolveRegionResult(countryCode?: string): Promise<RegionResolution> {\n const regions = this.regionsCache ?? (await this.getRegions())\n if (regions.length === 0) {\n // Fail closed: refuse to price without a resolved region (F-A2).\n throw new Error(\"no regions configured — cannot resolve a region for pricing\")\n }\n const home = regions[0]! // region #1 — the launch region (SEK today, derived not hardcoded)\n if (countryCode) {\n const cc = countryCode.toLowerCase()\n const match = regions.find((r) =>\n (r.countries ?? []).some((c) => (c.iso_2 ?? \"\").toLowerCase() === cc)\n )\n if (match) {\n return { region: match, matched: true, requested: cc, home: false }\n }\n // A country code was given but no region ships there. Warn (stderr, redacted) so the\n // mis-resolution is observable, and report matched:false so money surfaces fail closed.\n this.logWarn(\"store.region.unmatched\", { countryCode: cc, fallback: home.id })\n return { region: home, matched: false, requested: cc, home: false }\n }\n return { region: home, matched: false, requested: undefined, home: true }\n }\n\n /**\n * Resolve a region by country code, falling back to region #1. A thin wrapper over\n * resolveRegionResult for callers that only need the region (back-compat).\n * SEK is region #1 by configuration — NEVER a hardcoded literal.\n */\n async resolveRegion(countryCode?: string): Promise<RegionLike> {\n return (await this.resolveRegionResult(countryCode)).region\n }\n\n // ----- products (region resolved BEFORE pricing) ---------------------------\n\n async listProducts(opts?: { region?: RegionLike; countryCode?: string }): Promise<ProductLike[]> {\n const region = opts?.region ?? (await this.resolveRegion(opts?.countryCode))\n // Paginate (L12): a single limit:100 call silently drops the 101st product. Page through until a\n // short page, with a hard cap so a misbehaving API can never spin this unbounded.\n const PAGE = 100\n const HARD_CAP = 1000\n const all: ProductLike[] = []\n for (let offset = 0; offset < HARD_CAP; offset += PAGE) {\n const data = await this.request<{ products: ProductLike[] }>(\"GET\", \"/store/products\", {\n // +metadata: Medusa omits product.metadata by default, and that's where lore/lore_sources\n // (and any per-product spec fields) live — without this the CLI/MCP never see them.\n query: { region_id: region.id, limit: PAGE, offset, fields: PRODUCT_FIELDS },\n })\n const page = data.products ?? []\n all.push(...page)\n if (page.length < PAGE) {\n break\n }\n }\n return all\n }\n\n async getProduct(handle: string, opts?: { region?: RegionLike; countryCode?: string }): Promise<ProductLike | null> {\n const region = opts?.region ?? (await this.resolveRegion(opts?.countryCode))\n const data = await this.request<{ products: ProductLike[] }>(\"GET\", \"/store/products\", {\n query: { handle, region_id: region.id, limit: 1, fields: PRODUCT_FIELDS },\n })\n return (data.products ?? [])[0] ?? null\n }\n\n // ----- cart (mutations carry Idempotency-Key) ------------------------------\n\n async createCart(region: RegionLike): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\"POST\", \"/store/carts\", {\n idempotent: true,\n body: { region_id: region.id },\n })\n return data.cart\n }\n\n async addLineItem(cartId: string, variantId: string, quantity: number): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\n \"POST\",\n `/store/carts/${encodeURIComponent(cartId)}/line-items`,\n {\n idempotent: true,\n cartRecovery: true,\n body: { variant_id: variantId, quantity },\n }\n )\n return data.cart\n }\n\n async getCart(cartId: string): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\n \"GET\",\n `/store/carts/${encodeURIComponent(cartId)}`,\n { cartRecovery: true }\n )\n return data.cart\n }\n\n /** Remove a staged line item (bt unstage). Returns the updated cart (Medusa's `parent`). */\n async removeLineItem(cartId: string, itemId: string): Promise<CartLike> {\n const data = await this.request<{ parent?: CartLike; cart?: CartLike }>(\n \"DELETE\",\n `/store/carts/${encodeURIComponent(cartId)}/line-items/${encodeURIComponent(itemId)}`,\n { idempotent: true, cartRecovery: true }\n )\n return (data.parent ?? data.cart) as CartLike\n }\n\n /**\n * Ensure the cart has a shipping method before checkout (BUG 1 — the headless\n * /store/headless/checkout route guards on it, and neither client ever set one).\n *\n * - Idempotent: if the cart already carries a shipping method, no-op (no POST).\n * - Lists options via the STANDARD store route GET /store/shipping-options?cart_id=…\n * (NOT a /headless route — standard routes are always available, no kill-switch).\n * - Picks deterministically: exactly one → use it; several → the cheapest by amount.\n * - The SE launch option is region-scoped (price_type:\"flat\"), so listing by cart_id\n * returns it without a shipping address being set first (confirmed: seed-shipping.ts).\n *\n * TODO(04.5): buyer-choice / intl multiple shipping options — today we auto-pick the\n * cheapest; an explicit buyer chooser belongs to the Phase-6 international pass.\n */\n async ensureCartShippingMethod(cartId: string): Promise<CartLike> {\n // 1. read the authoritative cart; a stale/completed cart → RecoverableCartError (F-B2).\n const cart = await this.getCart(cartId)\n if (cart.shipping_methods && cart.shipping_methods.length > 0) {\n // idempotent: already satisfied.\n return cart\n }\n\n // 2. list options via the STANDARD store route (always available, no kill-switch).\n const data = await this.request<{ shipping_options: ShippingOptionLike[] }>(\n \"GET\",\n \"/store/shipping-options\",\n { query: { cart_id: cartId } }\n )\n const options = data.shipping_options ?? []\n if (options.length === 0) {\n // fail closed: never mint a checkout we know the backend will reject (BUG 1).\n throw new Error(\n \"no shipping option is available for this cart — cannot attach a shipping method\"\n )\n }\n\n // 3. pick deterministically: one → it; several → the cheapest by amount.\n const chosen =\n options.length === 1\n ? options[0]!\n : [...options].sort(\n (a, b) => (a.amount ?? Number.POSITIVE_INFINITY) - (b.amount ?? Number.POSITIVE_INFINITY)\n )[0]!\n\n // 4. attach it — a mutation, so it carries an Idempotency-Key + cart-recovery.\n const res = await this.request<{ cart: CartLike }>(\n \"POST\",\n `/store/carts/${encodeURIComponent(cartId)}/shipping-methods`,\n {\n idempotent: true,\n cartRecovery: true,\n body: { option_id: chosen.id },\n }\n )\n return res.cart\n }\n\n // ----- headless contract (04.5-01) -----------------------------------------\n\n // `channel` is REQUIRED (L11): a default silently mis-attributes analytics for a future caller that\n // forgets it. Both current callers (cli, mcp) pass it explicitly.\n async mintCheckout(cartId: string, email: string | undefined, channel: string): Promise<HeadlessCheckout> {\n return this.request<HeadlessCheckout>(\"POST\", \"/store/headless/checkout\", {\n idempotent: true,\n body: { cart_id: cartId, email, channel },\n })\n }\n\n async poll(sessionId: string): Promise<PollResult> {\n return this.request<PollResult>(\n \"GET\",\n `/store/headless/poll/${encodeURIComponent(sessionId)}`\n )\n }\n\n /** JWT-bound: returns the authenticated customer's own profile (G2). */\n async whoami(): Promise<WhoAmI> {\n // A guest (no/expired JWT) gets a 401/403 — that's the EXPECTED unauthenticated branch the\n // CLI turns into the friendly \"browsing as a guest\" line, NOT an error to dump (BUG 3).\n return this.request<WhoAmI>(\"GET\", \"/store/headless/whoami\", { expected4xx: [401, 403] })\n }\n\n // ----- magic-link auth (04.5-05) -------------------------------------------\n // Passwordless login — magic-link is the only grant (Medusa has no OAuth pairing-grant, G1).\n // request-link emails a single-use link; verify exchanges the link's token for a customer-scoped\n // JWT. The JWT is stored in the OS keychain by the CLI — never here, never in plaintext config (G7/G8).\n\n /** Start a magic-link login. Returns a UNIFORM ack regardless of whether the email exists (G12). */\n async requestMagicLink(email: string): Promise<RequestLinkResult> {\n return this.request<RequestLinkResult>(\"POST\", \"/store/headless/auth/request-link\", {\n idempotent: true,\n body: { email },\n })\n }\n\n /** Exchange a magic-link token for a customer-scoped JWT (the customer is materialized server-side). */\n async verifyMagicLink(token: string): Promise<VerifyLinkResult> {\n return this.request<VerifyLinkResult>(\"POST\", \"/store/headless/auth/verify\", {\n idempotent: true,\n body: { token },\n })\n }\n\n async listDrops(): Promise<DropRow[]> {\n const data = await this.request<{ drops: DropRow[]; count: number }>(\n \"GET\",\n \"/store/headless/drops\"\n )\n return data.drops ?? []\n }\n\n /**\n * File a developer bug report (d14 — `bt report` / MCP report_issue). Returns a correlation\n * reference. Additive: no money/cart path. The backend redacts authoritatively; callers SHOULD\n * pass an already-redacted context (the doctor snapshot). A mutation → carries an Idempotency-Key\n * so a retry can't email the report twice.\n */\n async report(message: string, context?: Record<string, unknown>): Promise<ReportResult> {\n return this.request<ReportResult>(\"POST\", \"/store/headless/report\", {\n idempotent: true,\n body: { message, ...(context !== undefined ? { context } : {}) },\n })\n }\n\n /**\n * The authenticated customer's order history (d13 — `bt log`). JWT-bound; the backend derives the\n * customer from the bearer token and returns MINIMAL fields (status/timeline only — never\n * email/address, F-H3). A guest (no/expired JWT) gets a 401 — the EXPECTED unauthenticated branch\n * we turn into the honest empty history (`[]`), NOT an error to dump (BUG 3).\n */\n async listOrders(): Promise<OrderSummary[]> {\n try {\n const data = await this.request<{ orders: OrderSummary[] }>(\"GET\", \"/store/headless/orders\", {\n expected4xx: [401, 403],\n })\n return data.orders ?? []\n } catch (e) {\n if (e instanceof StoreApiError && (e.status === 401 || e.status === 403)) {\n return []\n }\n throw e\n }\n }\n\n /**\n * Guest order lookup (AUD-8) — POST /store/order-lookup with the order id (display id OR internal\n * id) + the EXACT email in the BODY (never the query string, so the email is not access-logged).\n * The backend returns 404 on any mismatch (no enumeration) → the client surfaces null. Rate-limited\n * server-side; returns minimal status only, never PII (F-H1 / F-H3).\n */\n async getOrder(idOrDisplayId: string | number, email: string): Promise<unknown | null> {\n try {\n return await this.request<unknown>(\"POST\", \"/store/order-lookup\", {\n body: { id: String(idOrDisplayId), email },\n })\n } catch (e) {\n if (e instanceof StoreApiError && e.status === 404) {\n return null\n }\n throw e\n }\n }\n}\n","// Baked-in PUBLIC defaults for the Binary & Thread production channel.\n//\n// These let `npx @binaryandthread/cli` and the MCP server reach the live store with ZERO\n// configuration — the point of the soft-launch tarball (P4.5-7b) and decision #79 (d2).\n// store-client is bundled into both the CLI and the MCP server (tsup `noExternal`), so this\n// is the single source of truth for \"which store\" both clients talk to by default.\n//\n// SAFETY (CLAUDE.md rule 1 / SECURITY T6): the publishable key is PUBLIC BY DESIGN. It is the\n// SAME key embedded in the public storefront's client bundle (binaryandthread.com) and carries\n// no privilege beyond read + cart over the Store API — every dangerous operation lives behind\n// the backend. It is therefore NOT a secret; rule 1 governs secrets, and this is deliberately\n// published. Any env var (BT_API_URL / BT_STORE_URL / BT_PUBLISHABLE_KEY / MEDUSA_*) OVERRIDES\n// these for local dev or staging.\n\n/** The Binary & Thread production Medusa Store API base URL. */\nexport const BT_DEFAULT_STORE_URL = \"https://backend-production-511f5.up.railway.app\"\n\n// The PUBLIC Medusa publishable key (read + cart only; safe to ship — T6). Identical to the key\n// served in the public storefront bundle. NOT a secret (CLAUDE.md rule 1); the inline allow keeps\n// the gitleaks pre-commit hook from flagging a deliberately-published value.\nexport const BT_DEFAULT_PUBLISHABLE_KEY = \"pk_0d7d706860568a0414dff0436aadb9713a2609de3a1b153ef2a6f9694d23f907\" // gitleaks:allow\n","// Image-host allowlist — the canonical source for the WHOLE channel (CLAUDE.md rule 6 / SECURITY G9).\n//\n// We fetch product images ONLY from our own Vercel Blob store. Both the bt CLI (its terminal\n// image ladder) and the Wave-4 MCP (its server-side ImageContent fetch) gate every outbound\n// image fetch on this allowlist. It lives HERE in the shared client — not in the CLI — so the\n// MCP does not take a runtime dependency on the CLI package; the CLI re-exports it from here.\n//\n// HISTORY: a bare `endsWith(allowed)` suffix match was a CRITICAL allowlist-bypass bug fixed in\n// Wave 2 (it would admit `evilblob.vercel-storage.com`). The dot-delimited-subdomain check below\n// is load-bearing security — `host === allowed || host.endsWith(\".\" + allowed)`, never a bare\n// suffix. Do NOT \"simplify\" it.\n\n/**\n * Allowlisted image hosts. We fetch product images ONLY from our own Vercel Blob store\n * (public). NEVER a public temp host (uguu/0x0/catbox/transfer.sh) and NEVER an arbitrary\n * URL — that would be SSRF + a brand-IP leak (CLAUDE.md rule 6).\n */\nexport const BLOB_HOST_ALLOWLIST: readonly string[] = [\n \"blob.vercel-storage.com\", // matches the exact host + *.blob.vercel-storage.com (dot-delimited subdomains only)\n]\n\n/**\n * True if the URL's host is on the Blob allowlist.\n *\n * Exact host OR a true dot-delimited subdomain. NOT a bare suffix: a bare\n * `endsWith(allowed)` would admit `evilblob.vercel-storage.com` (allowlist bypass →\n * SSRF / brand-IP leak, CLAUDE.md rule 6). The dot guard is load-bearing.\n */\nexport function isAllowedImageHost(url: string): boolean {\n let host: string\n try {\n host = new URL(url).hostname.toLowerCase()\n } catch {\n return false\n }\n return BLOB_HOST_ALLOWLIST.some(\n (allowed) => host === allowed || host.endsWith(\".\" + allowed)\n )\n}\n","// Colour resolution — DERIVED from the variant matrix, never hardcoded (CAT-3 forward-compat).\n//\n// The dual-colour catalog (memory: dual-colour-catalog) gives each design ONE product with a\n// Colour × Size variant matrix. A variant's colour comes from, in order of trust:\n// 1. the variant option titled \"Colour\" (Model A: options {Size, Colour}, value title-cased)\n// 2. the variant title, format \"<size> / <Colour>\" (e.g. \"S / White\", \"M / Black\")\n// We NEVER hardcode white/black — future colours (CAT-3) fall out of the data for free.\n//\n// This lives in @binaryandthread/store-client (the canonical types home) so the CLI and the MCP\n// resolve colour the SAME way — written once, tested once.\n\nimport type { ProductLike, VariantLike, ImageLike } from \"./types\"\n\n/** Title-case a colour token for display (\"white\" → \"White\"); leaves multi-word reasonable. */\nexport function displayColour(colour: string): string {\n const c = colour.trim()\n if (!c) return c\n return c.charAt(0).toUpperCase() + c.slice(1).toLowerCase()\n}\n\n/**\n * The colour of a single variant, lowercased — or null if it carries no colour axis (a\n * legacy Size-only product). Reads the \"Colour\" option first, then parses the \"<size> / <Colour>\"\n * title. NEVER assumes a fixed palette.\n */\nexport function variantColour(v: VariantLike): string | null {\n // 1) the explicit Colour option (the authoritative source — memory: never parse the title).\n const opt = (v.options ?? []).find(\n (o) => (o.option?.title ?? \"\").toLowerCase() === \"colour\" || (o.option?.title ?? \"\").toLowerCase() === \"color\"\n )\n if (opt && typeof opt.value === \"string\" && opt.value.trim()) {\n return opt.value.trim().toLowerCase()\n }\n // 2) fall back to the title format \"<size> / <Colour>\".\n const title = (v.title ?? \"\").trim()\n if (title.includes(\"/\")) {\n const parts = title.split(\"/\").map((p) => p.trim())\n const tail = parts[parts.length - 1]\n if (tail) return tail.toLowerCase()\n }\n return null\n}\n\n/**\n * The DISTINCT colours offered by a product, in first-seen order, lowercased. Empty for a\n * legacy Size-only product (no Colour axis) — callers treat that as \"no colour to show\".\n */\nexport function productColours(p: ProductLike): string[] {\n const seen: string[] = []\n for (const v of p.variants ?? []) {\n const c = variantColour(v)\n if (c && !seen.includes(c)) {\n seen.push(c)\n }\n }\n return seen\n}\n\n/** The distinct sizes offered FOR a given colour (lowercased colour match), preserving order. */\nexport function sizesForColour(p: ProductLike, colour: string): string[] {\n const target = colour.trim().toLowerCase()\n const sizes: string[] = []\n for (const v of p.variants ?? []) {\n if (variantColour(v) !== target) continue\n const size = sizeOfVariant(v)\n if (size && !sizes.includes(size)) {\n sizes.push(size)\n }\n }\n return sizes\n}\n\n/** The size label of a variant — the Size option, else the title's leading \"<size>\" segment. */\nexport function sizeOfVariant(v: VariantLike): string | null {\n const opt = (v.options ?? []).find((o) => (o.option?.title ?? \"\").toLowerCase() === \"size\")\n if (opt && typeof opt.value === \"string\" && opt.value.trim()) {\n return opt.value.trim()\n }\n const title = (v.title ?? \"\").trim()\n if (title.includes(\"/\")) {\n const head = title.split(\"/\")[0]?.trim()\n if (head) return head\n }\n // No colour axis → the whole title IS the size (legacy Size-only product).\n return title || null\n}\n\n/**\n * Resolve a variant by size AND colour (the real buyability gap — bt stage / MCP add_to_cart).\n * - size + colour given → exact match on both (case-insensitive).\n * - size only → first variant of that size.\n * - colour only → first variant of that colour.\n * - neither → first variant.\n * Returns undefined when the requested size+colour combination does not exist (the caller MUST\n * surface a clear error — never silently substitute another colour the buyer didn't ask for).\n */\nexport function findVariant(\n p: ProductLike,\n opts: { size?: string; colour?: string } = {}\n): VariantLike | undefined {\n const variants = p.variants ?? []\n const size = opts.size?.trim().toLowerCase()\n const colour = opts.colour?.trim().toLowerCase()\n\n const sizeMatches = (v: VariantLike): boolean => {\n if (!size) return true\n return (sizeOfVariant(v) ?? \"\").toLowerCase() === size || (v.title ?? \"\").toLowerCase() === size\n }\n const colourMatches = (v: VariantLike): boolean => {\n if (!colour) return true\n return variantColour(v) === colour\n }\n\n if (size && colour) {\n // both specified → require BOTH; no fallback to a different colour (a wrong colour is worse\n // than a clear \"that combo isn't available\").\n return variants.find((v) => sizeMatches(v) && colourMatches(v))\n }\n if (colour && !size) {\n return variants.find((v) => colourMatches(v))\n }\n if (size && !colour) {\n // size specified → require it; NO fallback to variants[0] (L10) — a silently-wrong size is worse\n // than undefined, which lets the caller (stage.ts) surface a clear \"no matching variant\" error.\n return variants.find((v) => sizeMatches(v))\n }\n return variants[0]\n}\n\n// The gallery scene we surface in the terminal: the flat-lay \"plp\" shot (the tee laid flat on the\n// photographic/concrete plate) — the SAME scene as the storefront thumbnail. Pinning to one scene is\n// what makes every tee read CONSISTENTLY (never a mix of on-model here / hero there); the product\n// image order alone is unreliable (D9). Filenames are `<slug>-<scene>-<colour>.webp`.\nconst PREFERRED_SCENE = \"plp\"\n\n/** The URL of a specific SCENE for a colour (or any colour when `colour` is null) — matches\n * `-<scene>-` in the filename + the colour token. null if no such image. */\nfunction sceneImage(\n images: Array<ImageLike> | null | undefined,\n scene: string,\n colour: string | null\n): string | null {\n for (const img of images ?? []) {\n const u = (img?.url ?? \"\").trim()\n if (!u || !u.toLowerCase().includes(`-${scene}-`)) continue\n if (colour === null || new RegExp(`(^|[^a-z])${escapeRegex(colour)}([^a-z]|$)`, \"i\").test(u.toLowerCase())) {\n return u\n }\n }\n return null\n}\n\n/**\n * The image URL to render for a given colour. Resolves, in order:\n * 1. the flat-lay \"plp\" scene for the colour — the CONSISTENT, thumbnail-matching choice (D9), so\n * every tee shows the same flat-on-concrete shot rather than an order-dependent on-model/hero.\n * 2. the matching variant's own images[]\n * 3. a product image whose url/metadata names the colour\n * 4. null → the caller falls back to product.thumbnail + a \"black available\" note.\n * Never throws; a missing colour just yields null.\n */\nexport function imageForColour(p: ProductLike, colour: string): string | null {\n const target = colour.trim().toLowerCase()\n if (!target) {\n return sceneImage(p.images, PREFERRED_SCENE, null) ?? p.thumbnail ?? firstImageUrl(p.images) ?? null\n }\n\n // 1) the flat-lay plp scene for this colour — consistent across every product (matches thumbnail).\n const plp = sceneImage(p.images, PREFERRED_SCENE, target)\n if (plp) return plp\n\n // 2) the colour's variant images.\n for (const v of p.variants ?? []) {\n if (variantColour(v) !== target) continue\n const url = firstImageUrl(v.images)\n if (url) return url\n }\n\n // 3) a product image that names the colour (url substring or metadata.colour tag).\n for (const img of p.images ?? []) {\n const tag = (img.metadata?.[\"colour\"] ?? img.metadata?.[\"color\"] ?? \"\")\n .toString()\n .toLowerCase()\n if (tag === target) {\n const u = (img.url ?? \"\").trim()\n if (u) return u\n }\n }\n for (const img of p.images ?? []) {\n const u = (img.url ?? \"\").trim()\n if (u && new RegExp(`(^|[^a-z])${escapeRegex(target)}([^a-z]|$)`, \"i\").test(u)) {\n return u\n }\n }\n return null\n}\n\nfunction firstImageUrl(images: Array<ImageLike> | null | undefined): string | null {\n for (const img of images ?? []) {\n const u = (img?.url ?? \"\").trim()\n if (u) return u\n }\n return null\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n","// Canonical product-content accessors — the ONE source for a piece's README, spec sheet, and lore,\n// shared by the CLI (`bt view` / `bt blame`), the MCP (`get_product`), and (aspirationally) the\n// storefront PDP. Keeping these here means the three channels can't drift on what a piece \"says\".\n//\n// The spec fields + fallbacks mirror the storefront's buildSpecs (apps/storefront .../templates).\n\nimport type { ProductLike } from \"./types\"\n\n/** One row of the spec sheet. */\nexport interface SpecEntry {\n key: string\n value: string\n}\n\n/** A lore link (a real source behind the pun/idea). */\nexport interface LoreSource {\n url: string\n label: string\n}\n\n/** A piece's lore: the story text + any sources. Empty text = no lore on record. */\nexport interface ProductLore {\n text: string\n sources: LoreSource[]\n}\n\n/** A non-empty string, or the fallback. */\nfunction str(v: unknown, fallback: string): string {\n return typeof v === \"string\" && v.length > 0 ? v : fallback\n}\n\n/** The piece's README — the human description shown on the PDP's README toggle. */\nexport function productReadme(p: ProductLike): string {\n return typeof p.description === \"string\" ? p.description : \"\"\n}\n\n/**\n * The spec sheet — the 6 canonical rows, in PDP order, with the same fallbacks as the storefront's\n * buildSpecs (the single source of truth for what a piece is made of / how it ships).\n */\nexport function productSpecs(p: ProductLike): SpecEntry[] {\n const md = (p.metadata ?? {}) as Record<string, unknown>\n return [\n { key: \"material\", value: str(md.material, \"100% cotton\") },\n { key: \"weight\", value: str(md.weight, \"180 g/m²\") },\n { key: \"fit\", value: str(md.fit, \"Unisex heavyweight crewneck\") },\n { key: \"print\", value: str(md.print, \"Front print\") },\n { key: \"delivery\", value: str(md.delivery, \"Ships in 2–5 business days + transit\") },\n { key: \"care\", value: str(md.care, \"Cold wash, line dry\") },\n ]\n}\n\n/** The piece's lore — the story behind the pun + any real sources. Empty text = none on record. */\nexport function productLore(p: ProductLike): ProductLore {\n const md = (p.metadata ?? {}) as Record<string, unknown>\n const text = typeof md.lore === \"string\" ? md.lore : \"\"\n const raw = Array.isArray(md.lore_sources) ? md.lore_sources : []\n const sources: LoreSource[] = raw\n .filter(\n (s): s is { url: string; label: string } =>\n !!s &&\n typeof s === \"object\" &&\n typeof (s as { url?: unknown }).url === \"string\" &&\n typeof (s as { label?: unknown }).label === \"string\"\n )\n .map((s) => ({ url: s.url, label: s.label }))\n return { text, sources }\n}\n","// Tax-inclusivity — the single source of truth for the \"incl. VAT\" price label across EVERY\n// channel (storefront, the bt CLI, the MCP server). Kept here (in the shared client) so the CLI\n// and MCP agree with each other and with the backend on how the flag is read.\n\nimport type { CartLike } from \"./types\"\n\n/**\n * Whether a cart's prices are VAT-inclusive (EU/SE) vs tax-exclusive (US).\n *\n * Medusa exposes tax-inclusivity PER LINE ITEM (and per shipping method), never on the cart root —\n * there is no `cart.is_tax_inclusive` in @medusajs/types 2.15.5. So we DERIVE it from the items.\n * It is uniform per region (every line shares the region's tax setting), so `.some` is exact and a\n * mixed cart cannot occur. A US (tax-exclusive) cart → false → NO \"incl. VAT\" label; an SE cart →\n * true → \" incl. VAT\". This is NEVER keyed off a currency or country literal (INV-11): the label\n * follows the market's own tax flag, so it stays correct as new regions come online.\n *\n * Fails closed: an empty/absent cart or items array → false (no phantom VAT label on nothing).\n */\nexport function isCartTaxInclusive(cart: Pick<CartLike, \"items\"> | null | undefined): boolean {\n return (cart?.items ?? []).some((i) => i.is_tax_inclusive === true)\n}\n","// Voice helpers — the deadpan B&T Agent tone (VOICE.md).\n//\n// Rules these helpers enforce mechanically (VOICE §1.2):\n// - lowercase terminal body text (proper nouns kept by the caller; helpers don't shout)\n// - NO exclamation marks in the CLI, ever (logs don't shout)\n// - the established glyph set only: ✓ ✗ ⚠ ⚙ →\n//\n// Error doctrine (VOICE §1.4): every error names a plausible exit code (the joke), then\n// states what did NOT happen to your money/order (the reassurance) BEFORE the fix, then a\n// copy-pasteable fix LAST. If money is in scope, \"nothing was charged\" comes first.\n\nexport const GLYPH = {\n ok: \"✓\",\n fail: \"✗\",\n warn: \"⚠\",\n gear: \"⚙\",\n arrow: \"→\",\n} as const\n\n/** Strip exclamation marks — the single fastest way to sound like a mascot (VOICE §1.2). */\nfunction noShout(s: string): string {\n return s.replace(/!+/g, \".\")\n}\n\n/** A success log line. The flex is the fact stated flatly. */\nexport function ok(msg: string): string {\n return `${GLYPH.ok} ${noShout(msg)}`\n}\n\n/** A warning / confirm line. Calm, not alarmist. */\nexport function warn(msg: string): string {\n return `${GLYPH.warn} ${noShout(msg)}`\n}\n\n/** A progress/working line. */\nexport function working(msg: string): string {\n return `${GLYPH.gear} ${noShout(msg)}`\n}\n\n/** An empty-state line — the best place for one dry true line (low stakes, high charm). */\nexport function empty(msg: string): string {\n return noShout(msg)\n}\n\nexport interface FailSpec {\n /** A plausible-real exit code or HTTP status (the joke). */\n code: number | string\n /** what went wrong. */\n what: string\n /** money/order reassurance — printed BEFORE the fix when present (non-negotiable). */\n money?: string\n /** a copy-pasteable fix, printed last. */\n fix?: string\n /** an optional link line (e.g. the checkout URL). */\n link?: string\n}\n\n/**\n * An error block. Order is doctrine (VOICE §1.4):\n * line 1: ✗ exit <code> — <what>\n * line 2: <money reassurance> (only if money is in scope — comes first)\n * line 3: fix → <fix>\n * line 4: link → <link>\n */\nexport function fail(spec: FailSpec): string {\n const lines: string[] = []\n lines.push(`${GLYPH.fail} exit ${spec.code} — ${noShout(spec.what)}`)\n if (spec.money) {\n lines.push(` ${noShout(spec.money)}`)\n }\n if (spec.fix) {\n lines.push(` fix ${GLYPH.arrow} ${noShout(spec.fix)}`)\n }\n if (spec.link) {\n lines.push(` link ${GLYPH.arrow} ${spec.link}`)\n }\n return lines.join(\"\\n\")\n}\n\n// ----- easter-egg machinery (governed copy; the rest land in Wave-3 polish) --\n\n/**\n * C1 — the canonical teapot egg. Evergreen Core (★). Delivered straight, cites the RFC.\n * `bt brew coffee` → 418. Implemented here as the canonical example; the rest of the egg\n * catalog (C2–C18) is a governed Wave-3 polish pass (VOICE §3.5 five-gate sign-off).\n */\nexport function brewCoffee(): string {\n // Canonical copy now lives in eggs.teapot(); kept here for back-compat. Brand-voice compliant\n // (no \"print\"/\"shirts\" — memory brand-voice-no-pod-no-tshirt).\n return `${GLYPH.fail} 418 i'm a teapot. we make clothing, not coffee. (RFC 2324)`\n}\n\n/**\n * C4 — version codename. Codenames rotate per minor release; each is a real dev holy war,\n * stated deadpan.\n */\nconst CODENAMES = [\n \"tabs-over-spaces\",\n \"vim-not-emacs\",\n \"2-space-indent\",\n \"trailing-comma\",\n \"rebase-not-merge\",\n \"lf-not-crlf\",\n \"semicolons-optional\",\n \"dark-mode-only\",\n] as const\n\n/**\n * Rotate PER MINOR version — the joke should evolve across releases, not sit frozen — while staying\n * deterministic: same minor → same codename, so `bt --version` is reproducible for screenshots.\n * Falls back to index 0 for an unparseable version.\n */\nexport function codename(version: string): string {\n const minor = Number.parseInt(version.split(\".\")[1] ?? \"\", 10)\n const idx = Number.isFinite(minor) ? Math.abs(minor) % CODENAMES.length : 0\n return CODENAMES[idx]!\n}\n\n/** `bt --version` line: `bt <semver> \"<codename>\"`. */\nexport function versionLine(version: string): string {\n return `bt ${version} \"${codename(version)}\"`\n}\n","// Fatal-error classification → distinct exit codes + a JSON error envelope (d10).\n//\n// CI / agents need to tell auth from network from not-found from rate-limit — a blanket exit 1\n// can't be branched on. We map the shared client's typed errors to sysexits-style codes (the same\n// family the VOICE error doctrine already uses, e.g. 75 EX_TEMPFAIL for the checkout timeout), and\n// in --json mode emit a structured `{ error: { code, type, message } }` envelope. Every message is\n// passed through redact() so a token/email can never leak into a crash line (G10).\n\nimport { StoreApiError, RecoverableCartError, redact } from \"@binaryandthread/store-client\"\n\nexport const EXIT = {\n OK: 0,\n GENERIC: 1,\n DATAERR: 65, // EX_DATAERR — bad request / unprocessable / stale cart (400/409/422)\n UNAVAILABLE: 69, // EX_UNAVAILABLE — not found (404)\n TEMPFAIL: 75, // EX_TEMPFAIL — temporary: 5xx, network/exhausted, rate-limited (429)\n NOPERM: 77, // EX_NOPERM — auth required / forbidden (401/403)\n} as const\n\nexport interface ErrorInfo {\n code: number\n type: string\n status?: number\n message: string\n}\n\nfunction messageOf(e: unknown): string {\n if (e instanceof Error && e.message) {\n return e.message\n }\n const s = String(e ?? \"\").trim()\n return s || \"unexpected error\"\n}\n\n/** Map any thrown value to an exit code + a stable machine `type` + a redacted message. */\nexport function classifyError(e: unknown): ErrorInfo {\n const message = String(redact(messageOf(e)))\n if (e instanceof RecoverableCartError) {\n return { code: EXIT.DATAERR, type: \"stale_cart\", status: e.status, message }\n }\n if (e instanceof StoreApiError) {\n const s = e.status\n if (s === 401 || s === 403) return { code: EXIT.NOPERM, type: \"auth\", status: s, message }\n if (s === 404) return { code: EXIT.UNAVAILABLE, type: \"not_found\", status: s, message }\n if (s === 429) return { code: EXIT.TEMPFAIL, type: \"rate_limited\", status: s, message }\n // status 0 is the client's \"retries exhausted\" sentinel; 5xx/408 are upstream/temporary.\n if (s === 0 || s === 408 || s >= 500) return { code: EXIT.TEMPFAIL, type: \"upstream\", status: s, message }\n if (s >= 400) return { code: EXIT.DATAERR, type: \"client_error\", status: s, message }\n }\n return { code: EXIT.GENERIC, type: \"error\", message }\n}\n\n/**\n * What the top-level handler should write for a fatal error. Pure (no process access) so it's\n * unit-testable: returns the stdout (JSON envelope) OR stderr (human line) text + the exit code.\n */\nexport function formatFatal(e: unknown, json: boolean): { out: string | null; err: string | null; code: number } {\n const info = classifyError(e)\n if (json) {\n const envelope = { error: { code: info.code, type: info.type, message: info.message } }\n return { out: JSON.stringify(envelope, null, 2), err: null, code: info.code }\n }\n // The one cold surface used to be a bare `✗ <message>`. Keep the doctrine even on the fallback:\n // a calm, universally-true reassurance (the client is dumb — a crash loses nothing local), a\n // money line for the checkout case, and a pointer that turns a crash into a contributor moment.\n const reassurance = \"nothing was lost. if a checkout was in flight, your order is safe. file it → bt report\"\n return { out: null, err: `✗ ${info.message}\\n ${reassurance}`, code: info.code }\n}\n","// Brand surface — ASCII framing + SPARING cyan accent for the human-facing CLI screens.\n//\n// Design system (design-system/MASTER.md §1.1): on a dark terminal, cyan is the signature\n// accent — used on KEY elements only (prices, the ✓, headers), never washing everything.\n// Box-drawing rules give the IDE/terminal structure the brand wants (VOICE §1, B1).\n//\n// HARD constraints (do not break):\n// - --json output stays MACHINE-CLEAN: brand framing/colour is NEVER applied in --json mode.\n// - NO_COLOR / non-TTY / piped stdout → NO ANSI escapes (colour degrades to plain text).\n// - the box rules are pure ASCII-ish box-drawing chars (no colour) so they survive NO_COLOR.\n//\n// A BrandStyle is an explicit value object (color on/off) so rendering is deterministic and\n// unit-testable; makeBrand() builds the auto-detected one for bin.ts.\n\n/** ANSI codes — only emitted when color is enabled. */\nconst ANSI = {\n reset: \"\\x1b[0m\",\n // cyan: the signature accent. On a dark terminal this is the neon \"glow\" the design system\n // reserves for key elements. 38;5;48 ≈ #00ffc2 family on 256-colour terminals.\n cyan: \"\\x1b[38;5;48m\",\n // green: reserved for the contribution graph ONLY — the GitHub-contribution-green flex on the\n // `bt whoami` card. Not a general accent (that's cyan); a deliberate, single second colour.\n green: \"\\x1b[38;5;42m\",\n dim: \"\\x1b[2m\",\n bold: \"\\x1b[1m\",\n} as const\n\nexport interface BrandStyle {\n /** whether ANSI colour may be emitted (false under NO_COLOR / non-TTY / --no-color / --json). */\n readonly color: boolean\n}\n\n/** Detect whether colour should be emitted. NO_COLOR or a non-TTY stdout → no colour (degrade). */\nexport function colorEnabled(opts: { noColor?: boolean; isTTY?: boolean } = {}): boolean {\n if (opts.noColor) return false\n if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== \"\") return false\n const tty = opts.isTTY ?? Boolean(process.stdout.isTTY)\n return tty\n}\n\n/** Build a BrandStyle. In --json mode the caller passes color:false explicitly (machine-clean). */\nexport function makeBrand(opts: { noColor?: boolean; isTTY?: boolean } = {}): BrandStyle {\n return { color: colorEnabled(opts) }\n}\n\n/** A no-colour brand (used in --json paths and tests that assert plain text). */\nexport const PLAIN: BrandStyle = { color: false }\n\nfunction wrap(style: BrandStyle, code: string, s: string): string {\n if (!style.color) return s\n return `${code}${s}${ANSI.reset}`\n}\n\n/** cyan accent — KEY elements only (prices, the ✓, header). Degrades to plain text. */\nexport function cyan(style: BrandStyle, s: string): string {\n return wrap(style, ANSI.cyan, s)\n}\n\n/** dim — secondary/handle text (the handle shown beside the title). */\nexport function dim(style: BrandStyle, s: string): string {\n return wrap(style, ANSI.dim, s)\n}\n\n/** green — the contribution-graph colour ONLY (the `bt whoami` flex). Degrades to plain text. */\nexport function green(style: BrandStyle, s: string): string {\n return wrap(style, ANSI.green, s)\n}\n\n/** bold — used sparingly on headers. */\nexport function bold(style: BrandStyle, s: string): string {\n return wrap(style, ANSI.bold, s)\n}\n\n// ----- box drawing (pure chars, NO colour — survives NO_COLOR verbatim) --------\n\nconst BOX = {\n tl: \"┌\",\n tr: \"┐\",\n bl: \"└\",\n br: \"┘\",\n h: \"─\",\n v: \"│\",\n ml: \"├\",\n mr: \"┤\",\n} as const\n\n/**\n * A header treatment: a left-anchored title with a trailing rule, e.g.\n * ── bt browse ─────────────────────────────────\n * The title may carry a cyan accent; the rule is plain. Deadpan, uncluttered (B1).\n */\n/**\n * The brand mark, echoed in the terminal: a cyan underscore — the \"Binary & Thread _\" cursor. One\n * faint character; the single most on-brand micro-touch (delight #1). Static, NOT blinking: ANSI\n * blink is poorly supported and jarring, so the cyan glyph carries the identity on its own. Degrades\n * to a plain \"_\" under --no-color.\n */\nexport function cursor(style: BrandStyle): string {\n return cyan(style, \"_\")\n}\n\nexport function header(style: BrandStyle, title: string, width = 56): string {\n const label = ` ${title} `\n const lead = BOX.h.repeat(2)\n const used = lead.length + label.length\n // Reserve 2 cols for the trailing \" _\" cursor so the rule stays the same total width.\n const tail = BOX.h.repeat(Math.max(2, width - used - 2))\n return `${lead}${cyan(style, label)}${tail} ${cursor(style)}`\n}\n\n/** A plain horizontal rule of the given width (no colour). */\nexport function rule(width = 56): string {\n return BOX.h.repeat(width)\n}\n\n/**\n * Frame a block of pre-rendered lines in a light box. The frame chars are plain (NO_COLOR-safe);\n * the inner lines keep whatever accent they already carry. ANSI escapes inside a line are not\n * counted toward width (so coloured content still aligns). Padding to the requested inner width\n * is best-effort — the box is structure, not a strict table.\n */\nexport function box(lines: string[], opts: { width?: number; title?: string; style?: BrandStyle } = {}): string[] {\n const style = opts.style ?? PLAIN\n const inner = opts.width ?? 56\n const out: string[] = []\n // top: ┌── title ──────┐ (title optional)\n if (opts.title) {\n const label = ` ${opts.title} `\n const lead = BOX.h.repeat(2)\n const used = lead.length + visibleLength(label)\n const tail = BOX.h.repeat(Math.max(1, inner - used))\n out.push(`${BOX.tl}${lead}${cyan(style, label)}${tail}${BOX.tr}`)\n } else {\n out.push(`${BOX.tl}${BOX.h.repeat(inner)}${BOX.tr}`)\n }\n for (const line of lines) {\n const pad = Math.max(0, inner - 1 - visibleLength(line))\n out.push(`${BOX.v} ${line}${\" \".repeat(pad)}${BOX.v}`)\n }\n out.push(`${BOX.bl}${BOX.h.repeat(inner)}${BOX.br}`)\n return out\n}\n\n/** ANSI-escape-stripped length, so coloured content doesn't break alignment. */\nexport function visibleLength(s: string): number {\n // eslint-disable-next-line no-control-regex\n return s.replace(/\\x1b\\[[0-9;]*m/g, \"\").length\n}\n\n// ----- contribution graph (the `bt whoami` flex) -------------------------------\n\n/**\n * The contribution bar for `bt whoami` — one filled green cell per committed order, up to a cap\n * (the core threshold, so a full bar == you reached core). HONEST by construction: each filled cell\n * is one REAL captured order you committed — NOT a faked time-series. (A true GitHub-style per-month\n * grid needs order created_at buckets from the backend — a read-only follow-up, noted in STATE.)\n *\n * Filled cells carry the contribution-green; empty cells are plain ░ so the bar still reads under\n * NO_COLOR. The width is fixed (= cap) so the card aligns regardless of count.\n */\nexport function contributionBar(style: BrandStyle, commits: number, cap = 20): string {\n const n = Number.isFinite(commits) ? Math.max(0, Math.floor(commits)) : 0\n const filledCount = Math.min(n, cap)\n const filled = green(style, \"▇\".repeat(filledCount))\n const empty = \"░\".repeat(Math.max(0, cap - filledCount))\n return `${filled}${empty}`\n}\n\n// ----- colour markers (A3 INFORM) ----------------------------------------------\n\n/** The swatch glyph for a colour name — ⬜ white, ⬛ black, ◼ for anything else (deadpan). */\nexport function colourSwatch(colour: string): string {\n const c = colour.trim().toLowerCase()\n if (c === \"white\") return \"⬜\"\n if (c === \"black\") return \"⬛\"\n return \"◼\"\n}\n\n/**\n * A compact colour marker for a product row: the swatches + the names, e.g. \"⬜⬛ white/black\".\n * Empty string when the product has no colour axis (a legacy Size-only product).\n */\nexport function colourMarker(colours: string[]): string {\n if (colours.length === 0) return \"\"\n const swatches = colours.map((c) => colourSwatch(c)).join(\"\")\n const names = colours.join(\"/\")\n return `${swatches} ${names}`\n}\n","// Shared types + helpers for the bt read/cart command handlers.\n//\n// Each handler is a PURE function (client, config, args) → HandlerResult so it can be unit\n// tested without a live backend or a real filesystem. bin.ts wires them to commander and\n// performs the side effects (print lines, save config).\n\nimport type { Command } from \"commander\"\nimport { variantColour, isCartTaxInclusive } from \"@binaryandthread/store-client\"\nimport type { StoreClient, ProductLike, VariantLike, CartLineItemLike, BtConfigLike, RegionLike } from \"./types-bridge\"\n\n// Re-export so command handlers read the cart's VAT-inclusivity from one local place (./shared),\n// alongside taxSuffix. isCartTaxInclusive derives the flag from the cart's line items (Medusa has\n// no cart-root flag) — the single source of truth shared with the storefront + MCP.\nexport { isCartTaxInclusive }\nimport type { BrandStyle } from \"../brand\"\nimport { PLAIN, makeBrand, dim } from \"../brand\"\nimport { fail, GLYPH, type FailSpec } from \"../voice\"\nimport { EXIT } from \"../errors\"\nimport type { RegionResolution } from \"@binaryandthread/store-client\"\n\nexport interface HandlerResult {\n /** human-readable, voice-compliant output lines. */\n lines: string[]\n /** machine-readable payload for --json (when requested). */\n json?: unknown\n /** a config patch the router must persist atomically (e.g. { cart_id }). */\n save?: Partial<BtConfigLike>\n /**\n * A non-zero process exit code for a HANDLED failure (a fail() the command printed itself\n * instead of throwing). Without this, a gracefully-handled failure (e.g. a checkout that timed\n * out) prints \"exit 75\" but the process exits 0 — so CI/agents branching on $? see success (d10).\n * printResult applies it. Omit (or 0) for success + informational/user-choice states.\n */\n exitCode?: number\n}\n\n/**\n * The optional brand style threaded into a handler's args. Handlers default to PLAIN (no colour)\n * so tests assert deterministic plain text; bin.ts injects the auto-detected style for real runs.\n * In --json mode the router passes PLAIN so machine output is never coloured.\n */\nexport interface Branded {\n brand?: BrandStyle\n}\n\n/**\n * One dim, subtle provenance line — which region/currency this output is priced in — for the top of\n * discovery surfaces (browse/view/cart). The `git status`-style \"here's your context\" line: calm\n * when you've chosen a region, and a gentle nudge to pick one when you're on the home default. Never\n * shouts; suppressed in --json (callers gate on it). Country shown from the requested cc, else the\n * region's first country, else omitted (never a hardcoded literal).\n */\nexport function regionLine(rr: RegionResolution, style: BrandStyle): string {\n const cc = (rr.requested ?? rr.region.countries?.[0]?.iso_2 ?? \"\").toLowerCase()\n const loc = cc ? `${cc} · ` : \"\"\n const nudge = rr.home ? \" · not you? → bt region\" : \"\"\n return dim(style, `${GLYPH.gear} region: ${loc}${rr.region.currency_code}${nudge}`)\n}\n\n// Canonical garment size order — so a size list always reads S · M · L · XL · 2XL, never the\n// arbitrary variant order the API happens to return (E2). Unknown sizes sort to the end, alpha.\nconst SIZE_ORDER = [\"xxs\", \"xs\", \"s\", \"m\", \"l\", \"xl\", \"2xl\", \"xxl\", \"3xl\", \"xxxl\", \"4xl\", \"5xl\"]\n\n/** Sort garment sizes into the canonical XS→5XL order (case-insensitive; unknowns last). */\nexport function sortSizes(sizes: string[]): string[] {\n const rank = (s: string): number => {\n const i = SIZE_ORDER.indexOf(s.trim().toLowerCase())\n return i === -1 ? SIZE_ORDER.length : i\n }\n return [...sizes].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))\n}\n\n/** A handler's brand style, defaulting to PLAIN (no colour, no ANSI). */\nexport function brandOf(args: Branded | undefined): BrandStyle {\n return args?.brand ?? PLAIN\n}\n\nexport type ConfigGetter = () => BtConfigLike\nexport type ClientFactory = () => StoreClient\n\nexport interface CommandDeps {\n makeClient: ClientFactory\n loadConfig: ConfigGetter\n}\n\n/** Price as a flat SEK-ish line — currency comes from the variant/region, never a literal. */\nexport function fmtPrice(amount: number | null | undefined, currency: string | null | undefined): string {\n if (amount == null) {\n return \"—\"\n }\n const cur = (currency ?? \"\").toUpperCase()\n // amounts are stored as-is (not cents) per backend convention.\n return cur ? `${amount} ${cur}` : `${amount}`\n}\n\n/**\n * The tax suffix for a price line: \" incl. VAT\" ONLY for a tax-inclusive market (EU/SE), \"\" for a\n * tax-exclusive one (US — no VAT). Driven off the cart's / variant's OWN is_tax_inclusive flag,\n * NEVER a currency or country literal (INV-11). Pass isCartTaxInclusive(cart) for a cart total, or a\n * variant's calculated_price.is_calculated_price_tax_inclusive for a single-variant price line.\n */\nexport function taxSuffix(isTaxInclusive?: boolean | null): string {\n return isTaxInclusive ? \" incl. VAT\" : \"\"\n}\n\n/** First variant's calculated price + currency. */\nexport function firstPrice(p: ProductLike): {\n amount: number | null\n currency: string | null\n taxInclusive: boolean | null\n} {\n const v = (p.variants ?? [])[0]\n return {\n amount: v?.calculated_price?.calculated_amount ?? null,\n currency: v?.calculated_price?.currency_code ?? null,\n taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null,\n }\n}\n\n/** Resolve a variant by size label (case-insensitive option/title match). */\nexport function findVariantBySize(p: ProductLike, size?: string): VariantLike | undefined {\n const variants = p.variants ?? []\n if (!size) {\n return variants[0]\n }\n const target = size.toLowerCase()\n return (\n variants.find((v) => (v.title ?? \"\").toLowerCase() === target) ??\n variants.find((v) =>\n (v.options ?? []).some((o) => (o.value ?? \"\").toLowerCase() === target)\n ) ??\n variants[0]\n )\n}\n\n/** stage marker: ● in stock (main) / ○ on next. */\nexport function stageMarker(channelStage: string | undefined): string {\n return channelStage === \"next\" ? \"○ on next\" : \"● in stock\"\n}\n\n/**\n * The PRIMARY display name for a product (A1): the TITLE — the name checkout/web shows —\n * with the handle shown secondarily/dimmed (still needed for `bt view`/`bt stage`). Falls back\n * to the handle when a product somehow has no title. The handle keeps its case unchanged (A1).\n */\nexport function displayName(p: { title?: string | null; handle?: string | null; id?: string }): {\n title: string\n handle: string\n} {\n const handle = (p.handle ?? p.id ?? \"\").toString()\n const title = (p.title ?? \"\").toString().trim() || handle\n return { title, handle }\n}\n\n/**\n * The colour carried by a cart line — parsed from the variant title the server stores\n * (\"<size> / <Colour>\"), so `bt cart`/`status` can show it clearly (A3). Returns null when the\n * line has no colour axis. The line's `title` field is the product title; the colour rides the\n * variant title, surfaced separately by the server payload where available.\n */\nexport function lineColour(item: { variant_title?: string | null; title?: string | null }): string | null {\n const vt = (item.variant_title ?? \"\").toString()\n if (vt) {\n const c = variantColour({ id: \"x\", title: vt })\n if (c) return c\n }\n return null\n}\n\n/** A short, human line-item handle for display — the last 6 chars of the id (BUG 2). */\nexport function shortItemId(id: string | null | undefined): string {\n const s = (id ?? \"\").toString()\n return s.length <= 6 ? s : s.slice(-6)\n}\n\n/**\n * Render one staged line with a STABLE per-line reference (BUG 2): a 1-based index and the\n * short line-item id, so `bt unstage <ref>` has something concrete to target. Shows the colour\n * the line carries (A3) and accents the price in cyan when a brand style is given. Currency\n * comes from the cart/region, never a literal.\n */\nexport function formatCartLine(\n item: CartLineItemLike,\n index: number,\n currency: string | null | undefined,\n brand?: BrandStyle\n): string {\n const style = brand ?? PLAIN\n const ref = `[${index + 1}]`\n const title = (item.title ?? item.product_title ?? \"item\").toString()\n const idTag = item.id ? dimTag(style, ` (#${shortItemId(item.id)})`) : \"\"\n const colour = lineColour(item)\n const colourTag = colour ? ` · ${colour}` : \"\"\n const price = item.total != null ? ` ${accentPrice(style, fmtPrice(item.total, currency))}` : \"\"\n return ` ${ref} ${title}${colourTag}${idTag} · qty ${item.quantity}${price}`\n}\n\n/** dim a secondary tag (the short id) — plain text under NO_COLOR. Lazy import avoids a cycle. */\nfunction dimTag(style: BrandStyle, s: string): string {\n return style.color ? `\\x1b[2m${s}\\x1b[0m` : s\n}\n\n/** cyan-accent a price — plain text under NO_COLOR (design system: cyan on key elements only). */\nfunction accentPrice(style: BrandStyle, s: string): string {\n return style.color ? `\\x1b[38;5;48m${s}\\x1b[0m` : s\n}\n\n/**\n * Resolve a user-supplied `<ref>` against the cart's line items to a real line-item id (BUG 2).\n * Accepts, in order: a 1-based index (\"1\"), an exact line-item id, the short id (last 6 chars),\n * or a case-insensitive substring of the product title/handle. Returns null when nothing matches\n * (the caller MUST surface a clear error, never a false success).\n */\nexport function resolveLineItemRef(\n items: Array<CartLineItemLike>,\n ref: string\n): CartLineItemLike | null {\n const needle = (ref ?? \"\").trim()\n if (!needle || items.length === 0) {\n return null\n }\n\n // 1) a 1-based index.\n if (/^\\d+$/.test(needle)) {\n const idx = Number(needle) - 1\n if (idx >= 0 && idx < items.length) {\n return items[idx]!\n }\n }\n\n // 2) an exact line-item id, or the short (last-6) id.\n const byId = items.find((li) => li.id === needle || shortItemId(li.id) === needle)\n if (byId) {\n return byId\n }\n\n // 3) a case-insensitive substring of the title (the product word, e.g. \"bezier\"/\"terminal\").\n const lower = needle.toLowerCase()\n const byTitle = items.find((li) =>\n ((li.title ?? li.product_title ?? \"\").toString().toLowerCase()).includes(lower)\n )\n return byTitle ?? null\n}\n\n/** True when every char of `needle` appears in `hay` in order (a forgiving typo/abbrev match). */\nfunction isSubsequence(needle: string, hay: string): boolean {\n let i = 0\n for (let j = 0; j < hay.length && i < needle.length; j++) {\n if (hay[j] === needle[i]) i++\n }\n return i === needle.length\n}\n\n/** The outcome of resolving a product SELECTOR (a browse index, an exact slug, or a fuzzy hint). */\nexport interface ProductSelectorMatch {\n /** the single resolved product, when exactly one matched. */\n match?: ProductLike\n /** >1 fuzzy candidate (ambiguous) — the caller disambiguates. empty on a clean match or a miss. */\n candidates: ProductLike[]\n /** true when matched loosely (a browse index or a fuzzy hint), so the caller can say what it picked. */\n fuzzy: boolean\n}\n\n/**\n * Resolve a user SELECTOR against the catalog listing — the delightful, scriptable alternative to\n * an arrow-key maze (the review's \"the ceremony is the confirm, not the menu\"). Precedence:\n * 1) a bare 1-based index into the `bt browse` listing (\"3\" → the 3rd design)\n * 2) an EXACT handle/id (case-insensitive) — the scriptable spine; ALWAYS wins, never fuzzy\n * 3) a fuzzy hint: a case-insensitive subsequence of a handle or title (\"termnl\" → terminal-...)\n * Exactly one fuzzy hit → match (fuzzy:true, so the caller can note what it picked); 0 or >1 →\n * candidates for the caller to disambiguate. Pure — the async catalog fetch lives in resolveProduct.\n */\nexport function resolveProductSelector(products: ProductLike[], selector: string): ProductSelectorMatch {\n const needle = (selector ?? \"\").trim()\n if (!needle || products.length === 0) {\n return { candidates: [], fuzzy: false }\n }\n\n // 1) a 1-based index into the browse listing.\n if (/^\\d+$/.test(needle)) {\n const idx = Number(needle) - 1\n if (idx >= 0 && idx < products.length) {\n return { match: products[idx], candidates: [], fuzzy: true }\n }\n return { candidates: [], fuzzy: true } // out of range → a clean miss, still an index attempt\n }\n\n const lower = needle.toLowerCase()\n // 2) exact handle/id — the scriptable spine. Never treated as fuzzy.\n const exact = products.find((p) => (p.handle ?? \"\").toLowerCase() === lower || p.id === needle)\n if (exact) {\n return { match: exact, candidates: [], fuzzy: false }\n }\n\n // 3) fuzzy: a subsequence of the handle or the title.\n const hits = products.filter(\n (p) =>\n isSubsequence(lower, (p.handle ?? \"\").toLowerCase()) ||\n isSubsequence(lower, (p.title ?? \"\").toLowerCase())\n )\n if (hits.length === 1) {\n return { match: hits[0], candidates: [], fuzzy: true }\n }\n return { candidates: hits, fuzzy: true }\n}\n\n/** The resolved product (or the ambiguity) for a selector, after any needed catalog fetch. */\nexport interface ResolvedProduct {\n product?: ProductLike\n /** >1 fuzzy candidate — the caller shows them and asks the user to be specific. */\n candidates?: ProductLike[]\n /** matched loosely (index/fuzzy) rather than by exact handle. */\n fuzzy?: boolean\n}\n\n/**\n * Resolve a selector to a product. Exact-handle stays a single fast getProduct call (unchanged\n * behavior, so existing `bt stage <slug>` scripts are byte-for-byte unaffected); only a bare index\n * or a non-matching slug falls back to the catalog listing for index/fuzzy resolution.\n */\nexport async function resolveProduct(\n client: StoreClient,\n region: RegionLike,\n selector: string\n): Promise<ResolvedProduct> {\n const needle = (selector ?? \"\").trim()\n // exact fast-path (skip for a bare index — getProduct would 404 on a number).\n if (needle && !/^\\d+$/.test(needle)) {\n const exact = await client.getProduct(needle, { region })\n if (exact) {\n return { product: exact }\n }\n }\n const products = await client.listProducts({ region })\n const r = resolveProductSelector(products, needle)\n if (r.match) {\n return { product: r.match, fuzzy: r.fuzzy }\n }\n return { candidates: r.candidates, fuzzy: r.fuzzy }\n}\n\n/**\n * Resolve the global --json / --no-color flags from commander into the output mode every command\n * shares: the `json` boolean and the `brand` style. In --json mode colour is always off (machine\n * output is never coloured); otherwise colour follows --no-color (`opts.color === false`).\n */\nexport function resolveOutput(program: Command): { json: boolean; brand: BrandStyle } {\n const opts = program.opts() as { json?: boolean; color?: boolean }\n const json = Boolean(opts.json)\n const brand = json ? makeBrand({ noColor: true }) : makeBrand({ noColor: opts.color === false })\n return { json, brand }\n}\n\n/**\n * Build a HANDLED-failure result: the voice-compliant `fail()` line + the correct process exit\n * code + a machine `{ error: {...} }` envelope for --json. Before this helper, `fail({code})` set\n * only the DISPLAY text — commands had to also remember `exitCode` (some didn't → exit 0 on failure,\n * B3) and a `json` payload (none did → --json printed the human block to stdout, B2). Routing every\n * failure through here fixes both classes at once.\n *\n * `spec.code` is the DISPLAY code (may be a decorative HTTP status like 418 or 402). The process\n * exit code is `spec.exit` when given, else `spec.code` when it's a valid 1..255 exit, else GENERIC.\n */\nexport function failResult(spec: FailSpec & { exit?: number }): HandlerResult {\n const numericCode = typeof spec.code === \"number\" && spec.code >= 1 && spec.code <= 255\n const exitCode = spec.exit ?? (numericCode ? (spec.code as number) : EXIT.GENERIC)\n return {\n lines: [fail(spec)],\n exitCode,\n json: {\n error: {\n code: exitCode,\n message: spec.what,\n ...(spec.fix ? { fix: spec.fix } : {}),\n ...(spec.link ? { link: spec.link } : {}),\n },\n },\n }\n}\n\n/** Print a handler result (shared by the command actions). */\nexport function printResult(res: HandlerResult, json: boolean): void {\n // A handled failure carries a non-zero exit code so CI/agents can branch on $? — a fail() line\n // that only prints (and returns) would otherwise leave the process at exit 0 (d10). Thrown\n // errors still set the code via bin.ts's top-level catch; this covers the graceful paths.\n if (res.exitCode) {\n process.exitCode = res.exitCode\n }\n if (json) {\n // JSON mode: stdout is machine-only. Emit the payload if present; otherwise route the human\n // lines to stderr so a --json consumer never has to parse a voice block off stdout (B2).\n if (res.json !== undefined) {\n process.stdout.write(JSON.stringify(res.json, null, 2) + \"\\n\")\n } else {\n process.stderr.write(res.lines.join(\"\\n\") + \"\\n\")\n }\n return\n }\n process.stdout.write(res.lines.join(\"\\n\") + \"\\n\")\n}\n","// Image ladder (ROBUSTNESS §6, F-A3).\n//\n// Degrades: Kitty/iTerm2/Sixel inline (terminal-image auto-detects) → Unicode half-block\n// → plain text + the Blob URL FLOOR. The Blob URL is ALWAYS emitted as a text line so a\n// buy is never blocked on rendering. Catches every error → \"(image unavailable)\\n<url>\".\n//\n// CLAUDE.md rule 6 / SECURITY G9: only fetch images from OUR Vercel Blob host. Any other\n// host is refused before any network fetch (no SSRF, no brand-IP leak).\n//\n// The allowlist constant + check now live in @binaryandthread/store-client (the CANONICAL\n// source) so the Wave-4 MCP can reuse them WITHOUT depending on this CLI package. We re-export\n// them here so existing CLI imports (and tests) keep working unchanged. The dot-delimited\n// subdomain guard inside isAllowedImageHost is load-bearing security — see store-client.\nexport { BLOB_HOST_ALLOWLIST, isAllowedImageHost } from \"@binaryandthread/store-client\"\nimport { isAllowedImageHost } from \"@binaryandthread/store-client\"\n\nexport interface RenderImageOpts {\n /** Max render height in terminal rows (advisory; passed to terminal-image). */\n height?: number\n /** Respect NO_COLOR — when set we skip ANSI rendering and go straight to the floor. */\n noColor?: boolean\n /** Test seam: force the no-renderer path (URL-only floor). */\n forceNoRenderer?: boolean\n /** Test seam: inject a renderer. Resolves to the rendered string or throws. */\n renderer?: (url: string) => Promise<string>\n /** Test seam: override the byte cap (defaults to IMAGE_BYTE_CAP). */\n byteCap?: number\n}\n\n/**\n * The default render height in terminal rows. The tee mockup is portrait (≈4:5), so a taller\n * render reads sharper than the old 20-row default — both on the high-fidelity inline protocols\n * (iTerm2/Kitty, where rows ≈ image height) and on the half-block fallback (where each row packs\n * two pixels). 28 rows fits a standard terminal without scrolling the prompt off-screen.\n */\nconst DEFAULT_RENDER_HEIGHT = 28\n\n/** Hyperlink-ish floor line: just the plain URL (OSC-8 added by the caller's terminal where supported). */\nfunction urlFloor(url: string): string {\n return ` ${url}`\n}\n\n/** https-only, parity with the MCP image path — an http:/data: URL is never fetched (no plaintext, no inline payload). */\nfunction isHttpsUrl(url: string): boolean {\n try {\n return new URL(url).protocol === \"https:\"\n } catch {\n return false\n }\n}\n\n/**\n * Render an image for the terminal. NEVER throws. ALWAYS returns a string ending with\n * the Blob URL floor. The render is best-effort garnish; the URL is the guarantee.\n */\nexport async function renderImage(blobUrl: string, opts: RenderImageOpts = {}): Promise<string> {\n const noColor = opts.noColor ?? Boolean(process.env.NO_COLOR)\n\n // Refuse a non-allowlisted host OR a non-https URL BEFORE any fetch (rule 6 / G9 — parity with the\n // MCP image path: https-only means no http: plaintext and no data: inline payload). Still emit the floor.\n if (!isAllowedImageHost(blobUrl) || !isHttpsUrl(blobUrl)) {\n return ` (image host not allowed — showing link only)\\n${urlFloor(blobUrl)}`\n }\n\n // NO_COLOR or an explicit no-renderer request → URL-only floor.\n if (noColor || opts.forceNoRenderer) {\n return urlFloor(blobUrl)\n }\n\n try {\n const rendered = opts.renderer\n ? await opts.renderer(blobUrl)\n : await defaultRender(blobUrl, opts)\n return `${rendered}\\n${urlFloor(blobUrl)}`\n } catch {\n // Any failure (blob 404, no terminal protocol, fetch error) → graceful floor (F-A3, M9).\n return ` (image unavailable)\\n${urlFloor(blobUrl)}`\n }\n}\n\n/** Bound the Blob fetch — a hung CDN must not stall `bt view` (matches the MCP image path). */\nconst IMAGE_FETCH_TIMEOUT_MS = 5000\n\n/** ≤2MB — parity with the MCP image path (token/memory-DoS guard, G9). Over this → graceful URL floor. */\nconst IMAGE_BYTE_CAP = 2 * 1024 * 1024\n\n/** WebP magic bytes: \"RIFF\"<4 bytes>\"WEBP\". */\nfunction isWebp(buf: Buffer): boolean {\n return buf.length >= 12 && buf.toString(\"ascii\", 0, 4) === \"RIFF\" && buf.toString(\"ascii\", 8, 12) === \"WEBP\"\n}\n\n/**\n * Decode a WebP buffer to PNG so terminal-image can render it (terminal-image throws on WebP). A\n * non-WebP buffer passes straight through (terminal-image handles PNG/JPEG/GIF), so sharp is only\n * loaded when actually needed. Kept separate for a focused unit test.\n */\nexport async function toPng(buf: Buffer): Promise<Buffer> {\n if (!isWebp(buf)) return buf\n const sharp = (await import(\"sharp\")).default\n return sharp(buf).png().toBuffer()\n}\n\n/** Default renderer using terminal-image (lazy import — keeps the native sharp dep off cold paths). */\nasync function defaultRender(url: string, opts: RenderImageOpts): Promise<string> {\n // Lazy dynamic import so a CLI that never views an image doesn't pay the sharp load.\n const mod = (await import(\"terminal-image\")) as {\n default?: { buffer?: (buf: Buffer, o?: unknown) => Promise<string> }\n buffer?: (buf: Buffer, o?: unknown) => Promise<string>\n }\n const terminalImage = mod.default ?? mod\n const bufferFn = terminalImage.buffer\n if (!bufferFn) {\n throw new Error(\"terminal-image.buffer unavailable\")\n }\n // Timeout the fetch: a hung connection (no bytes, no error) would otherwise stall forever.\n // On abort/timeout this throws → caught by renderImage → graceful URL floor (F-A3).\n const ctrl = new AbortController()\n const timer = setTimeout(() => ctrl.abort(), IMAGE_FETCH_TIMEOUT_MS)\n let res: Response\n try {\n res = await fetch(url, { signal: ctrl.signal })\n } finally {\n clearTimeout(timer)\n }\n if (!res.ok) {\n throw new Error(`blob fetch failed: HTTP ${res.status}`)\n }\n // Byte cap (parity with the MCP image path): reject an over-cap body — pre-check the server-declared\n // size, then the actual buffer (Content-Length can be absent/wrong). Over-cap throws → caught by\n // renderImage → graceful URL floor (F-A3), never a huge buffer into memory.\n const cap = opts.byteCap ?? IMAGE_BYTE_CAP\n const declaredLen = Number.parseInt(res.headers?.get?.(\"content-length\") ?? \"\", 10)\n if (Number.isFinite(declaredLen) && declaredLen > cap) {\n throw new Error(\"blob exceeds byte cap (declared)\")\n }\n const rawBuf = Buffer.from(await res.arrayBuffer())\n if (rawBuf.byteLength === 0 || rawBuf.byteLength > cap) {\n throw new Error(\"blob empty or exceeds byte cap\")\n }\n // terminal-image CANNOT decode WebP (\"Mime type image/webp does not support decoding\"), and every\n // B&T product image IS WebP (the storefront pre-optimizes to WebP) — so without this the inline\n // mockup was ALWAYS \"(image unavailable)\". Normalize to PNG via sharp first (sharp decodes\n // webp/png/jpeg; it's a native dep terminal-image already pulls in). On a non-WebP input this is a\n // cheap re-encode; on any sharp failure the outer renderImage catch still yields the URL floor.\n const buf = await toPng(rawBuf)\n // B5 — sharpness: terminal-image's `buffer()` already auto-detects the HIGH-FIDELITY inline\n // protocols (Kitty graphics, then iTerm2 via term-img) and ONLY falls back to the chunky Unicode\n // half-block when neither is present. We pass `preferNativeRender: true` EXPLICITLY so a future\n // default change can't silently force the half-block path, and `preserveAspectRatio: true` so the\n // portrait tee isn't squashed. The height is tuned up from 20 → 28 rows for a sharper render; we\n // leave width unset so it derives from the image's own aspect ratio (capped to the terminal).\n return bufferFn(buf, {\n height: opts.height ?? DEFAULT_RENDER_HEIGHT,\n preserveAspectRatio: true,\n preferNativeRender: true,\n })\n}\n","// bt view <handle> — render a tee: specs (sizes/variants/price) + the image via the ladder,\n// ALWAYS with the Blob URL as the text floor (VOICE §2.1, F-A3).\n//\n// A1: TITLE is the primary name; the handle is shown (dimmed) so the user knows what to pass to\n// bt stage/bt view.\n// A3: a `colours: ⬜ white · ⬛ black` line, sizes grouped per colour, and `--colour black` renders\n// the BLACK mockup image (resolved from the variant/product images; falls back to the white\n// image + a \"black available\" note when not resolvable).\n// B1: a light header treatment; cyan price accent. Degrades under --json / NO_COLOR.\n\nimport type { Command } from \"commander\"\nimport { empty, working } from \"../voice\"\nimport { productColours, sizesForColour, imageForColour, displayColour, productSpecs, productLore } from \"@binaryandthread/store-client\"\nimport { renderImage } from \"../image-ladder\"\nimport { fmtPrice, taxSuffix, displayName, brandOf, failResult, regionLine, sortSizes, printResult, resolveOutput, resolveProduct, type HandlerResult, type Branded } from \"./shared\"\nimport { EXIT } from \"../errors\"\nimport { header, cyan, dim, colourSwatch } from \"../brand\"\nimport type { StoreClient, BtConfigLike } from \"./types-bridge\"\n\nexport interface ViewArgs extends Branded {\n handle: string\n colour?: string\n json?: boolean\n noColor?: boolean\n}\n\nexport async function runView(\n client: StoreClient,\n config: BtConfigLike,\n args: ViewArgs\n): Promise<HandlerResult> {\n const style = brandOf(args)\n const rr = await client.resolveRegionResult(config.region)\n const region = rr.region\n // Resolve a selector (browse index / exact slug / fuzzy hint) → product. Exact handle stays a\n // single fast lookup (unchanged behavior); an index or fuzzy hint resolves against the catalog.\n const picked = await resolveProduct(client, region, args.handle)\n const product = picked.product\n\n if (!product) {\n // dry not-found — exit-code framing, no '!'\n if (picked.candidates && picked.candidates.length > 1) {\n const r = failResult({ code: EXIT.DATAERR, what: `\"${args.handle}\" matches ${picked.candidates.length} designs — pick one:` })\n r.lines.push(...picked.candidates.map((p) => ` ${p.handle ?? p.id}`), \" or be exact → bt view <slug>\")\n return r\n }\n return failResult({\n code: EXIT.UNAVAILABLE,\n what: `no design here called \"${args.handle}\".`,\n fix: \"bt browse (to see what's in stock)\",\n })\n }\n\n const { title, handle } = displayName(product)\n const colours = productColours(product)\n\n const lines: string[] = []\n // a loose match (index / fuzzy) → say what we picked before rendering it.\n if (picked.fuzzy && handle !== args.handle.trim()) {\n lines.push(working(`matched \"${handle}\" from \"${args.handle}\"`))\n }\n lines.push(header(style, title))\n lines.push(` ${dim(style, handle)}`)\n if (product.description) {\n lines.push(empty(` ${product.description}`))\n }\n\n const fabric = (product.metadata?.[\"fabric\"] as string) ?? null\n // The spec sheet — the SAME 6 canonical rows the PDP shows (single-sourced productSpecs), so\n // `bt view` reads like the piece's page in the terminal. Supersedes the old ad-hoc fabric line.\n const specs = productSpecs(product)\n for (const s of specs) {\n lines.push(` ${s.key.padEnd(9)}${s.value}`)\n }\n\n // colours line + sizes grouped per colour (A3).\n if (colours.length > 0) {\n lines.push(` colours ${colours.map((c) => `${colourSwatch(c)} ${c}`).join(\" · \")}`)\n for (const c of colours) {\n const sizes = sortSizes(sizesForColour(product, c)) // canonical S→5XL order (E2)\n lines.push(` ${colourSwatch(c)} ${c.padEnd(6)} ${sizes.join(\" \")}`)\n }\n } else {\n // legacy Size-only product — the flat size list, canonically sorted (E2).\n const sizes = sortSizes((product.variants ?? []).map((v) => v.title ?? \"?\"))\n lines.push(` sizes ${sizes.join(\" \")}`)\n }\n\n const v0 = (product.variants ?? [])[0]\n if (v0?.calculated_price) {\n lines.push(\n ` price ${cyan(style, fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code))}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`\n )\n lines.push(` ${regionLine(rr, style)}`) // one dim provenance line beside the price (T3.2)\n }\n\n // pick the image to render: the requested colour's mockup, or the default thumbnail (A3).\n let imageUrl: string | null = product.thumbnail ?? null\n let blackNote: string | null = null\n if (args.colour) {\n const wanted = args.colour.trim().toLowerCase()\n const resolved = imageForColour(product, wanted)\n if (resolved) {\n imageUrl = resolved\n } else {\n // can't resolve the colour-specific image → render the default + a clear note (A3).\n blackNote = ` ${displayColour(wanted)} available — showing the default mockup (no ${wanted} render on file).`\n }\n }\n\n if (imageUrl) {\n const rendered = await renderImage(imageUrl, { noColor: args.noColor })\n lines.push(rendered)\n }\n if (blackNote) {\n lines.push(blackNote)\n }\n if (colours.length > 1 && !args.colour) {\n const others = colours.filter((c) => c !== colours[0]).join(\", \")\n lines.push(` see another colour → bt view ${handle} --colour ${others.split(\",\")[0]?.trim()}`)\n }\n // point at the story — the lore/provenance. Only when there's lore on record (blame handles the\n // empty case gracefully, but no sense advertising it when there's nothing to read).\n if (productLore(product).text) {\n lines.push(` the story → bt blame ${handle}`)\n }\n\n return {\n lines,\n json: args.json\n ? {\n id: product.id,\n handle: product.handle,\n title: product.title,\n fabric,\n specs,\n colours,\n thumbnail: product.thumbnail,\n variants: (product.variants ?? []).map((v) => ({\n id: v.id,\n size: v.title,\n price: v.calculated_price?.calculated_amount ?? null,\n currency: v.calculated_price?.currency_code ?? null,\n })),\n }\n : undefined,\n }\n}\n\nexport function registerView(\n program: Command,\n makeClient: () => StoreClient,\n loadConfig: () => BtConfigLike\n): void {\n program\n .command(\"view\")\n .alias(\"show\")\n .argument(\"<handle>\", \"the design: a slug, a bt browse number (3), or a fuzzy hint (termnl)\")\n .option(\"--colour <colour>\", \"render a specific colour's mockup (e.g. black)\")\n .description(\"render a design: specs + image (always with the link as the floor).\")\n .action(async (handle: string, opts: { colour?: string }) => {\n const { json, brand } = resolveOutput(program)\n const noColor = (program.opts() as { color?: boolean }).color === false\n const res = await runView(makeClient(), loadConfig(), {\n handle,\n colour: opts.colour,\n json,\n noColor,\n brand,\n })\n printResult(res, json)\n })\n}\n","// bt stage <handle> [--size L] — reserve stock (git add).\n//\n// Flow (F-B1/F-B2/F-B3):\n// 1. resolve the product + variant (size → variant; default from config.defaultSize)\n// 2. get-or-create a cart (reuse config.cart_id when present, else createCart)\n// 3. addLineItem; on a RecoverableCartError (404/409/cart.completed) DISCARD the local\n// cart_id, create a FRESH cart, re-add, and TELL the user — never silently lose the item\n// 4. persist the cart_id via config.save (the router persists it atomically) BEFORE returning\n// 5. always re-read the cart and render the authoritative server state\n\nimport type { Command } from \"commander\"\nimport { ok, warn, working } from \"../voice\"\nimport { formatCartLine, brandOf, failResult, printResult, resolveOutput, resolveProduct, type HandlerResult, type Branded } from \"./shared\"\nimport { EXIT } from \"../errors\"\nimport { RecoverableCartError, findVariant, productColours } from \"@binaryandthread/store-client\"\nimport type { StoreClient, BtConfigLike, RegionLike, CartLike } from \"./types-bridge\"\n\nexport interface StageArgs extends Branded {\n handle: string\n size?: string\n colour?: string\n json?: boolean\n /** resolve + preview the variant WITHOUT mutating any cart (d21). */\n dryRun?: boolean\n}\n\nexport async function runStage(\n client: StoreClient,\n config: BtConfigLike,\n args: StageArgs\n): Promise<HandlerResult> {\n const style = brandOf(args)\n // Fail closed (A1): staging is where the cart's region+currency get bound. If the configured\n // ship-to country isn't one we have a region for, refuse — never create a cart priced in a\n // substituted currency. Discovery (browse/view) may show the home region; money may not. The\n // no-country home default (rr.home) is a legitimate default and proceeds.\n const rr = await client.resolveRegionResult(config.region)\n if (rr.requested && !rr.matched) {\n return failResult({\n code: EXIT.DATAERR,\n what: `we don't ship to ${rr.requested} yet.`,\n money: \"nothing staged, nothing charged.\",\n fix: \"bt region --list (see where we ship, then bt region <cc>)\",\n })\n }\n const region: RegionLike = rr.region\n\n // Resolve a SELECTOR (a browse index, an exact slug, or a fuzzy hint) → a product. An exact\n // handle stays a single fast lookup, so `bt stage <slug>` scripts are unchanged; a bare index\n // (\"3\") or a typo'd slug (\"termnl\") now resolve instead of erroring (the arrow-key alternative).\n const resolved = await resolveProduct(client, region, args.handle)\n if (!resolved.product) {\n if (resolved.candidates && resolved.candidates.length > 1) {\n // ambiguous input → non-zero so `bt stage x && bt checkout` halts (d10)\n const r = failResult({ code: EXIT.DATAERR, what: `\"${args.handle}\" matches ${resolved.candidates.length} designs — pick one:` })\n r.lines.push(...resolved.candidates.map((p) => ` ${p.handle ?? p.id}`), \" or be exact → bt stage <slug>\")\n return r\n }\n // not-found → non-zero (d10)\n return failResult({ code: EXIT.UNAVAILABLE, what: `no design here called \"${args.handle}\".`, fix: \"bt browse\" })\n }\n const product = resolved.product\n // A loose match (index / fuzzy) → say what we picked, so `bt stage 3` or `bt stage termnl` is\n // never a mystery. An exact handle match stays silent.\n const matchNote =\n resolved.fuzzy && (product.handle ?? \"\") !== args.handle.trim()\n ? working(`matched \"${product.handle}\" from \"${args.handle}\"`)\n : null\n\n const size = args.size ?? config.defaultSize\n // colour: explicit flag → config.defaultColour → first colour the product offers (A3, never\n // hardcoded). A Size-only product has no colour axis (colours = []) and ignores colour.\n const colours = productColours(product)\n const colour =\n args.colour ?? config.defaultColour ?? (colours.length > 0 ? colours[0] : undefined)\n\n const variant = findVariant(product, { size, colour })\n if (!variant) {\n // a clear error when the size+colour combo doesn't exist — never silently pick another colour.\n const have = colours.length ? ` available colours: ${colours.join(\", \")}.` : \"\"\n // unprocessable size+colour combo → non-zero (d10)\n return failResult({\n code: EXIT.DATAERR,\n what:\n `no variant for ${args.handle}${size ? `#size=${size}` : \"\"}` +\n `${colour ? ` (${colour})` : \"\"}.`,\n fix: `bt view ${args.handle} (to see available sizes + colours)${have}`,\n })\n }\n\n // --dry-run (d21): we've resolved a real variant — show what WOULD be staged, mutate nothing.\n // Re-running a dry-run is perfectly idempotent (no cart side-effects), unlike a real stage.\n if (args.dryRun) {\n const colourSuffix = colour ? ` (${colour})` : \"\"\n return {\n lines: [\n ...(matchNote ? [matchNote] : []),\n ok(`dry-run — would stage ${product.handle ?? args.handle}${size ? `#${size}` : \"\"}${colourSuffix} (nothing staged).`),\n \" run without --dry-run to acquire the inventory lock.\",\n ],\n json: args.json\n ? {\n dry_run: true,\n would_stage: {\n handle: product.handle ?? args.handle,\n size: size ?? null,\n colour: colour ?? null,\n variant_id: variant.id,\n },\n }\n : undefined,\n }\n }\n\n const lines: string[] = []\n if (matchNote) {\n lines.push(matchNote)\n }\n let cartId = config.cart_id\n let recovered = false\n\n // get-or-create the cart\n if (!cartId) {\n const fresh = await client.createCart(region)\n cartId = fresh.id\n }\n\n // add the line item, recovering from a stale/completed cart (F-B2)\n try {\n await client.addLineItem(cartId, variant.id, 1)\n } catch (e) {\n if (e instanceof RecoverableCartError) {\n // discard the stale cart_id, create a fresh one, re-add — never lose the item\n const fresh = await client.createCart(region)\n cartId = fresh.id\n await client.addLineItem(cartId, variant.id, 1)\n recovered = true\n } else {\n throw e\n }\n }\n\n // re-read the authoritative server state (F-B3)\n const cart = await client.getCart(cartId)\n\n if (recovered) {\n lines.push(\n warn(\n \"your previous cart had expired or was already committed — created a fresh cart and re-added it.\"\n )\n )\n }\n // the staged ref carries the colour now (A3): \"the-viber#L (black)\".\n const colourSuffix = colour ? ` (${colour})` : \"\"\n lines.push(\n ok(\n `staged ${product.handle ?? args.handle}${size ? `#${size}` : \"\"}${colourSuffix} · inventory lock acquired (held 10m)`\n )\n )\n ;(cart.items ?? []).forEach((item, i) => {\n lines.push(formatCartLine(item, i, cart.currency_code, style))\n })\n const itemCount = (cart.items ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0)\n lines.push(` ${itemCount} item${itemCount === 1 ? \"\" : \"s\"} staged → bt commit (or bt buy to do both)`)\n\n return {\n // persist the cart_id atomically BEFORE returning (the router calls config.save)\n save: { cart_id: cartId },\n lines,\n json: args.json\n ? { cart_id: cartId, recovered, colour: colour ?? null, items: cart.items ?? [], total: cart.total ?? null }\n : undefined,\n }\n}\n\nexport function registerStage(\n program: Command,\n makeClient: () => StoreClient,\n loadConfig: () => BtConfigLike\n): void {\n // saveConfig is imported lazily to keep handlers pure + testable.\n program\n .command(\"stage\")\n .alias(\"add\")\n .argument(\"<handle>\", \"the design: a slug (terminal-velocity), a bt browse number (3), or a fuzzy hint (termnl)\")\n .option(\"--size <size>\", \"garment size (default: your last commit's size)\")\n .option(\"--colour <colour>\", \"colour, e.g. white / black (default: the product's first colour)\")\n .option(\"--dry-run\", \"resolve the variant + show what would be staged, without staging\")\n .description(\"reserve stock — acquires an inventory lock, reserves nothing permanent.\")\n .action(async (handle: string, opts: { size?: string; colour?: string; dryRun?: boolean }) => {\n const { json, brand } = resolveOutput(program)\n const res = await runStage(makeClient(), loadConfig(), {\n handle,\n size: opts.size,\n colour: opts.colour,\n dryRun: opts.dryRun,\n json,\n brand,\n })\n if (res.save) {\n // atomic persist (config.saveConfig uses temp-file + rename — F-B1)\n const { saveConfig } = await import(\"../config\")\n saveConfig(res.save)\n }\n printResult(res, json)\n })\n}\n"],"mappings":"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","names":["out","empty","colourSuffix"]}
@@ -7,7 +7,7 @@ import {
7
7
  runStage,
8
8
  runView,
9
9
  sizesForColour
10
- } from "./chunk-NJ3DW5AX.js";
10
+ } from "./chunk-RDJJLYXB.js";
11
11
  import "./chunk-7OCVIDC7.js";
12
12
 
13
13
  // src/interactive.ts
@@ -104,4 +104,4 @@ async function stageInteractive(client, config, product, brand, select, print, s
104
104
  export {
105
105
  runInteractiveBrowse
106
106
  };
107
- //# sourceMappingURL=interactive-5PBJCENZ.js.map
107
+ //# sourceMappingURL=interactive-DJXIUVBM.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binaryandthread/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "bt — the Binary & Thread terminal store. Buy heavyweight pieces with the ceremony of a production deploy. A dumb, well-tested client over the Store API; cards never touch the terminal.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -43,5 +43,8 @@
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=20"
46
+ },
47
+ "optionalDependencies": {
48
+ "sharp": "^0.35.0"
46
49
  }
47
50
  }