@binaryandthread/cli 0.1.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.
@@ -0,0 +1 @@
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 { productColours, sizesForColour, findVariant } from \"@binaryandthread/store-client\"\nimport { runView } from \"./commands/view\"\nimport { runStage } from \"./commands/stage\"\nimport { displayName } from \"./commands/shared\"\nimport { colourSwatch, type BrandStyle } from \"./brand\"\nimport type { StoreClient, ProductLike, BtConfigLike } from \"./commands/types-bridge\"\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: Partial<BtConfigLike>) => 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 = productColours(p)\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, 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 product: ProductLike,\n brand: BrandStyle,\n select: SelectFn,\n print: (lines: string[]) => void,\n saveConfig?: (patch: Partial<BtConfigLike>) => void\n): Promise<void> {\n const { handle } = displayName(product)\n const colours = productColours(product)\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 ? sizesForColour(product, colour)\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, { size, colour })\n if (!variant) {\n print([\n `✗ exit 1 — no variant for ${handle}${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, 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":[]}
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-7OCVIDC7.js";
3
+
4
+ // src/poll.ts
5
+ var DEFAULT_INTERVAL_MS = 5e3;
6
+ var DEFAULT_DEADLINE_MS = 20 * 60 * 1e3;
7
+ function realSleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ async function pollUntilPlaced(client, sessionId, opts = {}) {
11
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
12
+ const deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS;
13
+ const now = opts.now ?? Date.now;
14
+ const sleep = opts.sleep ?? realSleep;
15
+ const start = now();
16
+ let attempt = 0;
17
+ for (; ; ) {
18
+ attempt += 1;
19
+ opts.onTick?.(attempt);
20
+ const result = await client.poll(sessionId);
21
+ if (result.status === "placed") {
22
+ return { status: "placed", order_id: result.order_id };
23
+ }
24
+ if (result.status === "expired") {
25
+ return { status: "expired", order_id: result.order_id };
26
+ }
27
+ if (now() - start >= deadlineMs) {
28
+ return { status: "timeout", order_id: null };
29
+ }
30
+ await sleep(intervalMs);
31
+ if (now() - start >= deadlineMs) {
32
+ return { status: "timeout", order_id: null };
33
+ }
34
+ }
35
+ }
36
+ export {
37
+ pollUntilPlaced
38
+ };
39
+ //# sourceMappingURL=poll-O3VUD5WV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/poll.ts"],"sourcesContent":["// Bounded poll loop — the checkout-completion watcher (F-C5 / F-I3).\n//\n// After the browser hop, the CLI polls the bt_session until the order is placed. This loop\n// is the single most safety-critical control flow in the write path that runs unattended:\n// it MUST be bounded so a closed-browser / never-returns buyer can never hang the terminal\n// forever (F-I3). It polls every `intervalMs` (5s), stops the instant the session is\n// \"placed\" or \"expired\", and stops at a `deadlineMs` (20-min cap) returning \"timeout\".\n//\n// The clock + sleep are INJECTED (defaults wired to the real ones) so the deadline is\n// deterministically testable without burning 20 minutes of wall-clock — and so nothing\n// authoritative is ever computed from an un-injectable clock (F-I5).\n\nimport type { PollResult } from \"@binaryandthread/store-client\"\n\n/** The minimal client surface the poll loop needs (so tests can mock just this). */\nexport interface PollClientLike {\n poll(sessionId: string): Promise<PollResult>\n}\n\nexport interface PollOptions {\n /** poll cadence in ms (default 5s). */\n intervalMs?: number\n /** hard deadline in ms from the first attempt (default 20 min). After this → \"timeout\". */\n deadlineMs?: number\n /** injectable monotonic clock (default Date.now). */\n now?: () => number\n /** injectable sleep (default a real setTimeout-backed sleep). */\n sleep?: (ms: number) => Promise<void>\n /** optional progress callback (the spinner / status line in the CLI). */\n onTick?: (attempt: number) => void\n}\n\n/** The terminal result of the poll loop. \"timeout\" is OUR status, distinct from the server's. */\nexport interface PollOutcome {\n status: \"placed\" | \"expired\" | \"timeout\"\n order_id: string | null\n}\n\nconst DEFAULT_INTERVAL_MS = 5_000\nconst DEFAULT_DEADLINE_MS = 20 * 60 * 1_000 // 20 minutes (F-C5)\n\nfunction realSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Poll `client.poll(sessionId)` until the order is placed, the session expires, or the\n * deadline is hit. NEVER loops forever: the deadline is the hard ceiling (F-I3). Cancellable\n * by the caller via SIGINT — the cart stays intact server-side, so a Ctrl-C is fully\n * resumable with `bt checkout --resume` (no local in-flight state to corrupt, F-I1).\n */\nexport async function pollUntilPlaced(\n client: PollClientLike,\n sessionId: string,\n opts: PollOptions = {}\n): Promise<PollOutcome> {\n const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS\n const deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? realSleep\n\n const start = now()\n let attempt = 0\n\n for (;;) {\n attempt += 1\n opts.onTick?.(attempt)\n\n const result = await client.poll(sessionId)\n if (result.status === \"placed\") {\n return { status: \"placed\", order_id: result.order_id }\n }\n if (result.status === \"expired\") {\n return { status: \"expired\", order_id: result.order_id }\n }\n\n // Still pending. If we've reached the deadline, stop — never loop forever (F-I3).\n if (now() - start >= deadlineMs) {\n return { status: \"timeout\", order_id: null }\n }\n\n await sleep(intervalMs)\n\n // Re-check the deadline AFTER sleeping so we never start another poll past the cap\n // (bounded overshoot: at most one interval beyond the deadline).\n if (now() - start >= deadlineMs) {\n return { status: \"timeout\", order_id: null }\n }\n }\n}\n"],"mappings":";;;;AAsCA,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB,KAAK,KAAK;AAEtC,SAAS,UAAU,IAA2B;AAC5C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAQA,eAAsB,gBACpB,QACA,WACA,OAAoB,CAAC,GACC;AACtB,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,QAAQ,KAAK,SAAS;AAE5B,QAAM,QAAQ,IAAI;AAClB,MAAI,UAAU;AAEd,aAAS;AACP,eAAW;AACX,SAAK,SAAS,OAAO;AAErB,UAAM,SAAS,MAAM,OAAO,KAAK,SAAS;AAC1C,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,EAAE,QAAQ,UAAU,UAAU,OAAO,SAAS;AAAA,IACvD;AACA,QAAI,OAAO,WAAW,WAAW;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,OAAO,SAAS;AAAA,IACxD;AAGA,QAAI,IAAI,IAAI,SAAS,YAAY;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,IAC7C;AAEA,UAAM,MAAM,UAAU;AAItB,QAAI,IAAI,IAAI,SAAS,YAAY;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ makePrompts
4
+ } from "./chunk-IM3HF7NN.js";
5
+ import "./chunk-7OCVIDC7.js";
6
+ export {
7
+ makePrompts
8
+ };
9
+ //# sourceMappingURL=prompts-GTC5VYM6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/man/bt.1 ADDED
@@ -0,0 +1,87 @@
1
+ .TH BT 1 "2026" "Binary & Thread" "bt manual"
2
+ .SH NAME
3
+ bt \- the Binary & Thread terminal store
4
+ .SH SYNOPSIS
5
+ .B bt
6
+ .I command
7
+ .RI [ options ]
8
+ .br
9
+ .B npx @binaryandthread/cli
10
+ .I command
11
+ .RI [ options ]
12
+ .SH DESCRIPTION
13
+ Binary & Thread is a techno-minimalist clothing & lifestyle company.
14
+ .B bt
15
+ is its terminal store: you browse, stage, and commit pieces with the
16
+ ceremony of a deploy. Payment opens in your browser \(em cards never
17
+ touch the terminal.
18
+ .SH COMMANDS
19
+ .TP
20
+ .B init
21
+ first-run welcome + the happy path
22
+ .TP
23
+ .B browse
24
+ list what's in stock
25
+ .TP
26
+ .B view
27
+ render one piece (image or ASCII) + specs
28
+ .TP
29
+ .B stage
30
+ reserve stock (like git add)
31
+ .TP
32
+ .B commit
33
+ turn your staged items into an order
34
+ .TP
35
+ .B buy
36
+ stage + commit in one
37
+ .TP
38
+ .B log
39
+ your order history as a git log (\fB--graph\fR, \fB--oneline\fR)
40
+ .TP
41
+ .B order
42
+ track one order through its lifecycle
43
+ .TP
44
+ .B whoami
45
+ your contribution profile + standing
46
+ .TP
47
+ .B drops
48
+ what's on next, before it hits the web
49
+ .TP
50
+ .B login
51
+ passwordless magic-link sign-in
52
+ .TP
53
+ .B report
54
+ file a bug report (you become a contributor)
55
+ .SH ENVIRONMENT
56
+ .TP
57
+ .B NO_COLOR
58
+ disable ANSI colour + inline images
59
+ .TP
60
+ .B BT_TOKEN
61
+ a customer JWT for non-interactive sessions
62
+ .TP
63
+ .B BT_STORE_URL
64
+ override the store base URL (defaults baked in)
65
+ .SH "EXIT STATUS"
66
+ .TP
67
+ .B 0
68
+ success
69
+ .TP
70
+ .B 1
71
+ general error
72
+ .TP
73
+ .B 75
74
+ EX_TEMPFAIL \(em a transient failure; nothing was charged, retry
75
+ .SH BUGS
76
+ none known. if you find one, you're now a contributor.
77
+ .br
78
+ file it:
79
+ .B bt report
80
+ (or bugs@binaryandthread.com)
81
+ .SH AUTHORS
82
+ The crew at Binary & Thread, and every contributor who committed a fit.
83
+ earned, not bought.
84
+ .SH "SEE ALSO"
85
+ .BR git (1),
86
+ .BR curl (1),
87
+ a mirror. go build something.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@binaryandthread/cli",
3
+ "version": "0.1.0",
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
+ "private": false,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "bin": {
9
+ "bt": "./dist/bin.js"
10
+ },
11
+ "man": "./man/bt.1",
12
+ "files": [
13
+ "dist",
14
+ "man",
15
+ "README.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "prepack": "npm run build",
23
+ "typecheck": "tsc --noEmit -p tsconfig.json",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest"
26
+ },
27
+ "dependencies": {
28
+ "@inquirer/select": "5.2.1",
29
+ "@napi-rs/keyring": "^1.3.0",
30
+ "commander": "^14.0.0",
31
+ "open": "11.0.0",
32
+ "ora": "^5.4.1",
33
+ "qrcode-terminal": "0.12.0",
34
+ "terminal-image": "4.3.0"
35
+ },
36
+ "devDependencies": {
37
+ "@binaryandthread/store-client": "0.1.0",
38
+ "@types/node": "^20.12.11",
39
+ "@types/qrcode-terminal": "0.12.2",
40
+ "tsup": "^8.5.1",
41
+ "typescript": "^5.6.2",
42
+ "vitest": "^3.2.6"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ }
47
+ }