@binaryandthread/cli 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/interactive.ts"],"sourcesContent":["// Interactive product picker (B3) — ADDITIVE, never the only path.\n//\n// When `bt browse` runs in a TTY without --json and not NO_COLOR-forced-plain, offer an\n// arrow-key select: ↑/↓ to move, enter to jump into the chosen product (renders its `view`\n// detail), then a small action prompt (stage [pick size + colour] / back / quit). The\n// scriptable command spine (`bt view <handle>`, `bt stage <handle> --size L --colour black`)\n// stays fully working — this is purely additive.\n//\n// The prompt primitive (@inquirer/select, pinned 5.2.1) is injected behind a SelectFn seam so\n// the picker logic is unit-testable without a TTY. On a non-TTY / unavailable prompt / a thrown\n// abort, runInteractiveBrowse returns false → the caller falls through to the static table.\n\nimport { findVariant, productFits, coloursFor, sizesFor, priceFor, fitLabel, type Fit } from \"@binaryandthread/store-client\"\nimport { shipToCountry } from \"./ship-to\"\nimport { runView } from \"./commands/view\"\nimport { runStage } from \"./commands/stage\"\nimport { displayName, fmtPrice } from \"./commands/shared\"\nimport { colourSwatch, type BrandStyle } from \"./brand\"\nimport type { StoreClient, ProductLike, BtConfigLike } from \"./commands/types-bridge\"\nimport type { ConfigPatch } from \"./config\"\n\n/** One choice in a select menu. */\nexport interface SelectChoice<T> {\n name: string\n value: T\n description?: string\n}\n\n/** The injected select primitive — resolves to the chosen value, or throws on cancel (Ctrl-C). */\nexport type SelectFn = <T>(opts: { message: string; choices: Array<SelectChoice<T>> }) => Promise<T>\n\nexport interface InteractiveDeps {\n select?: SelectFn\n /** print a block of lines (the view detail / stage result). Defaults to stdout. */\n print?: (lines: string[]) => void\n /** persist a config patch (the cart_id after a stage). Defaults to the real saveConfig. */\n saveConfig?: (patch: ConfigPatch) => void\n}\n\nexport interface InteractiveBrowseArgs {\n branch?: string\n brand: BrandStyle\n}\n\n// Menu sentinels — string values (not symbols) so the picker is unit-testable with a scripted\n// select and so they never collide with a product handle (the product values are objects).\nconst BACK = \"__back__\" as const\nconst QUIT = \"__quit__\" as const\n\n/** Load the real @inquirer/select lazily (keeps it off the cold non-interactive path). */\nasync function defaultSelect(): Promise<SelectFn | null> {\n try {\n const mod = (await import(\"@inquirer/select\")) as { default?: unknown }\n const fn = (mod.default ?? mod) as SelectFn\n return typeof fn === \"function\" ? fn : null\n } catch {\n return null\n }\n}\n\n/**\n * Run the interactive browse → view → stage loop. Returns true when it handled the command\n * (the user navigated and we rendered something), false when it should fall through to the\n * static table (no select primitive, no products, or an immediate cancel).\n */\nexport async function runInteractiveBrowse(\n client: StoreClient,\n config: BtConfigLike,\n args: InteractiveBrowseArgs,\n deps: InteractiveDeps = {}\n): Promise<boolean> {\n const select = deps.select ?? (await defaultSelect())\n if (!select) {\n return false // no prompt primitive → static table (the scriptable spine).\n }\n const print = deps.print ?? ((lines: string[]) => process.stdout.write(lines.join(\"\\n\") + \"\\n\"))\n\n const region = await client.resolveRegion(config.region)\n const products = await client.listProducts({ region })\n if (products.length === 0) {\n return false // nothing to pick → let the static path show the empty state.\n }\n\n try {\n // outer loop: pick a product → view it → action → maybe back to the list.\n for (;;) {\n const choices: Array<SelectChoice<ProductLike | string>> = products.map((p) => {\n const { title } = displayName(p)\n const colours = coloursFor(p, null)\n const marker = colours.length ? ` ${colours.map((c) => colourSwatch(c)).join(\"\")}` : \"\"\n return { name: `${title}${marker}`, value: p, description: p.handle ?? undefined }\n })\n choices.push({ name: \"quit\", value: QUIT })\n\n const picked = await select<ProductLike | string>({\n message: \"pick a design (↑/↓, enter)\",\n choices,\n })\n if (picked === QUIT) {\n return true\n }\n const product = picked as ProductLike\n\n // render the chosen product's view detail (reuse the real handler).\n const { handle } = displayName(product)\n const view = await runView(client, config, { handle, brand: args.brand, noColor: !args.brand.color })\n print(view.lines)\n\n // action prompt: stage / back / quit.\n const action = await select<string>({\n message: \"what next\",\n choices: [\n { name: \"stage this (pick size + colour)\", value: \"stage\" },\n { name: \"back to the list\", value: BACK },\n { name: \"quit\", value: QUIT },\n ],\n })\n if (action === QUIT) return true\n if (action === BACK) continue\n\n // stage: pick colour (if the product has a colour axis) then size, resolve, stage.\n await stageInteractive(client, config, region, product, args.brand, select, print, deps.saveConfig)\n return true\n }\n } catch {\n // a cancel (Ctrl-C) or any prompt error → we've already shown the table is the fallback.\n // Returning true avoids double-printing; the user explicitly entered the interactive flow.\n return true\n }\n}\n\n/** The size+colour stage sub-flow inside the picker. */\nasync function stageInteractive(\n client: StoreClient,\n config: BtConfigLike,\n region: Parameters<typeof shipToCountry>[1],\n product: ProductLike,\n brand: BrandStyle,\n select: SelectFn,\n print: (lines: string[]) => void,\n saveConfig?: (patch: ConfigPatch) => void\n): Promise<void> {\n const { handle } = displayName(product)\n // CAT-31: a design sold in two fits asks for the fit FIRST (with each fit's price); the colour and size\n // lists below are then that fit's, filtered to what this ship-to country can buy.\n const fits = productFits(product)\n // TF26: the same rule as `bt stage` (requested country, else the home region's first), via one helper.\n const countryCode = shipToCountry(config.region, region)\n let fit: Fit | undefined\n if (fits.length > 1) {\n fit = await select<Fit>({\n message: \"fit\",\n choices: fits.map((f) => {\n const pr = priceFor(product, f)\n return { name: `${fitLabel(f)} · ${fmtPrice(pr.amount, pr.currency)}`, value: f }\n }),\n })\n } else if (fits.length === 1) {\n fit = fits[0]\n }\n const colours = coloursFor(product, fit ?? null, countryCode)\n\n let colour: string | undefined\n if (colours.length > 1) {\n colour = await select<string>({\n message: \"colour\",\n choices: colours.map((c) => ({ name: `${colourSwatch(c)} ${c}`, value: c })),\n })\n } else if (colours.length === 1) {\n colour = colours[0]\n }\n\n // sizes available for the chosen colour (or all sizes for a Size-only product).\n const sizes =\n colour && colours.length\n ? sizesFor(product, fit ?? null, colour, countryCode) // CAT-37: only sizes this blank prints here\n : (product.variants ?? []).map((v) => v.title ?? \"?\").filter((s) => s !== \"?\")\n const size =\n sizes.length > 0\n ? await select<string>({ message: \"size\", choices: sizes.map((s) => ({ name: s, value: s })) })\n : undefined\n\n // resolve to confirm the combo exists BEFORE staging (clear error, no wrong-colour substitution).\n const variant = findVariant(product, { fit, size, colour })\n if (!variant) {\n print([\n `✗ exit 1 — no variant for ${handle}${fit ? ` --fit ${fit}` : \"\"}${size ? `#size=${size}` : \"\"}${colour ? ` (${colour})` : \"\"}.`,\n ` fix → bt view ${handle} (to see what's available)`,\n ])\n return\n }\n\n const res = await runStage(client, config, { handle, size, colour, fit, brand })\n if (res.save && saveConfig) {\n saveConfig(res.save)\n } else if (res.save) {\n const { saveConfig: realSave } = await import(\"./config\")\n realSave(res.save)\n }\n print(res.lines)\n}\n"],"mappings":"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","names":[]}