@binaryandthread/cli 0.1.3 → 0.1.5

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.
package/dist/bin.js CHANGED
@@ -7,10 +7,11 @@ import {
7
7
  } from "./chunk-OVG37IIX.js";
8
8
  import {
9
9
  makePrompts
10
- } from "./chunk-IM3HF7NN.js";
10
+ } from "./chunk-AAN4VWP3.js";
11
11
  import {
12
12
  BT_DEFAULT_PUBLISHABLE_KEY,
13
13
  BT_DEFAULT_STORE_URL,
14
+ EXIT,
14
15
  GLYPH,
15
16
  RecoverableCartError,
16
17
  StoreApiError,
@@ -27,24 +28,27 @@ import {
27
28
  firstPrice,
28
29
  fmtPrice,
29
30
  formatCartLine,
31
+ formatFatal,
30
32
  green,
31
33
  header,
32
34
  lineColour,
33
35
  ok,
34
36
  printResult,
35
37
  productColours,
38
+ productLore,
36
39
  redact,
37
40
  registerStage,
38
41
  registerView,
39
42
  resolveLineItemRef,
40
43
  resolveOutput,
44
+ resolveProduct,
41
45
  rule,
42
46
  runStage,
43
47
  stageMarker,
44
48
  versionLine,
45
49
  warn,
46
50
  working
47
- } from "./chunk-7FH7BZ3I.js";
51
+ } from "./chunk-VRNJLVHX.js";
48
52
  import {
49
53
  __require
50
54
  } from "./chunk-7OCVIDC7.js";
@@ -73,7 +77,7 @@ function makeLogger(debug, write = (s) => process.stderr.write(s)) {
73
77
  }
74
78
 
75
79
  // src/version.ts
76
- var VERSION = "0.1.3";
80
+ var VERSION = "0.1.5";
77
81
 
78
82
  // src/eggs.ts
79
83
  function teapot() {
@@ -102,6 +106,21 @@ function rmWardrobe() {
102
106
  function force() {
103
107
  return "--force-pushing to prod, are we. there's no --force here \u2014\n a commit to your wardrobe is already that decisive.";
104
108
  }
109
+ function gitClone() {
110
+ return "no clone for a fit \u2014 each one's committed by hand. browse the originals \u2192 bt browse";
111
+ }
112
+ function gitStash() {
113
+ return "that's bt unstage here. your cart holds; nothing's lost.";
114
+ }
115
+ function gitPush() {
116
+ return "no remote for your wardrobe. the only push that ships \u2192 bt commit";
117
+ }
118
+ function gitPull() {
119
+ return "nothing to pull \u2014 the freshest designs land as drops \u2192 bt drops";
120
+ }
121
+ function gitReflog() {
122
+ return "nothing's ever truly lost here. your history \u2192 bt log";
123
+ }
105
124
  var FORTUNES = [
106
125
  '// "there are only two hard things in computer science: cache invalidation and naming things." \u2014 phil karlton',
107
126
  '// "premature optimization is the root of all evil." \u2014 donald knuth',
@@ -180,50 +199,6 @@ function manPage(version) {
180
199
  ];
181
200
  }
182
201
 
183
- // src/errors.ts
184
- var EXIT = {
185
- OK: 0,
186
- GENERIC: 1,
187
- DATAERR: 65,
188
- // EX_DATAERR — bad request / unprocessable / stale cart (400/409/422)
189
- UNAVAILABLE: 69,
190
- // EX_UNAVAILABLE — not found (404)
191
- TEMPFAIL: 75,
192
- // EX_TEMPFAIL — temporary: 5xx, network/exhausted, rate-limited (429)
193
- NOPERM: 77
194
- // EX_NOPERM — auth required / forbidden (401/403)
195
- };
196
- function messageOf(e) {
197
- if (e instanceof Error && e.message) {
198
- return e.message;
199
- }
200
- const s = String(e ?? "").trim();
201
- return s || "unexpected error";
202
- }
203
- function classifyError(e) {
204
- const message = String(redact(messageOf(e)));
205
- if (e instanceof RecoverableCartError) {
206
- return { code: EXIT.DATAERR, type: "stale_cart", status: e.status, message };
207
- }
208
- if (e instanceof StoreApiError) {
209
- const s = e.status;
210
- if (s === 401 || s === 403) return { code: EXIT.NOPERM, type: "auth", status: s, message };
211
- if (s === 404) return { code: EXIT.UNAVAILABLE, type: "not_found", status: s, message };
212
- if (s === 429) return { code: EXIT.TEMPFAIL, type: "rate_limited", status: s, message };
213
- if (s === 0 || s === 408 || s >= 500) return { code: EXIT.TEMPFAIL, type: "upstream", status: s, message };
214
- if (s >= 400) return { code: EXIT.DATAERR, type: "client_error", status: s, message };
215
- }
216
- return { code: EXIT.GENERIC, type: "error", message };
217
- }
218
- function formatFatal(e, json) {
219
- const info = classifyError(e);
220
- if (json) {
221
- const envelope = { error: { code: info.code, type: info.type, message: info.message } };
222
- return { out: JSON.stringify(envelope, null, 2), err: null, code: info.code };
223
- }
224
- return { out: null, err: `\u2717 ${info.message}`, code: info.code };
225
- }
226
-
227
202
  // src/is-main.ts
228
203
  import { realpathSync } from "fs";
229
204
  import { fileURLToPath } from "url";
@@ -312,19 +287,20 @@ function deleteToken(deps = {}) {
312
287
  }
313
288
 
314
289
  // src/commands/browse.ts
315
- function productRow(p, stage, style) {
290
+ function productRow(p, index, stage, style) {
316
291
  const { amount, currency } = firstPrice(p);
317
292
  const { title, handle } = displayName(p);
318
293
  const colours = colourMarker(productColours(p));
294
+ const ref = dim(style, `[${index + 1}]`.padEnd(4));
319
295
  const parts = [
320
296
  title.padEnd(22),
321
297
  cyan(style, fmtPrice(amount, currency).padEnd(11)),
322
- dim(style, handle.padEnd(34)),
298
+ dim(style, handle.padEnd(30)),
323
299
  stageMarker(stage)
324
300
  ];
325
- const row = parts.join(" ").trimEnd();
301
+ const row = `${ref} ${parts.join(" ")}`.trimEnd();
326
302
  return colours ? `${row}
327
- ${dim(style, colours)}` : row;
303
+ ${dim(style, colours)}` : row;
328
304
  }
329
305
  async function runBrowse(client, config, args) {
330
306
  const style = brandOf(args);
@@ -344,9 +320,9 @@ async function runBrowse(client, config, args) {
344
320
  lines.push(" peek at next \u2192 bt browse --branch=next");
345
321
  } else {
346
322
  lines.push(header(style, "bt browse \u2014 in stock"));
347
- for (const { p, stage } of rows) {
348
- lines.push(productRow(p, stage, style));
349
- }
323
+ rows.forEach(({ p, stage }, i) => {
324
+ lines.push(productRow(p, i, stage, style));
325
+ });
350
326
  for (const d of drops) {
351
327
  const dt = (d.title ?? d.handle ?? d.id).toString();
352
328
  lines.push(
@@ -361,7 +337,7 @@ async function runBrowse(client, config, args) {
361
337
  if (drops.length > 0) {
362
338
  lines.push(" on next, before the web \u2192 bt drops");
363
339
  }
364
- lines.push(" view one \u2192 bt view <handle> \xB7 stage one \u2192 bt stage <handle> --size L");
340
+ lines.push(" view one \u2192 bt view <n|slug> \xB7 stage one \u2192 bt stage <n|slug> --size L");
365
341
  }
366
342
  return {
367
343
  lines,
@@ -377,10 +353,10 @@ async function runBrowse(client, config, args) {
377
353
  };
378
354
  }
379
355
  function registerBrowse(program, makeClient2, loadConfig2) {
380
- program.command("browse").alias("ls").description("list what's in stock on a branch.").option("--branch <branch>", "main (live) or next (terminal-only drops)").option("--plain", "force the static table (skip the interactive picker)").action(async (opts) => {
356
+ program.command("browse").alias("ls").description("list what's in stock on a branch.").option("--branch <branch>", "main (live) or next (terminal-only drops)").option("--pick", "opt into the arrow-key picker (default is the numbered table \u2014 use bt stage <n>)").option("--plain", "force the static table (the default; kept for back-compat)").action(async (opts) => {
381
357
  const { json, brand } = resolveOutput(program);
382
- if (!json && !opts.plain && brand.color && process.stdout.isTTY) {
383
- const { runInteractiveBrowse } = await import("./interactive-DZ67P6UX.js");
358
+ if (opts.pick && !json && brand.color && process.stdout.isTTY && process.stdin.isTTY) {
359
+ const { runInteractiveBrowse } = await import("./interactive-WFI5K77G.js");
384
360
  const handled = await runInteractiveBrowse(makeClient2(), loadConfig2(), { branch: opts.branch, brand });
385
361
  if (handled) return;
386
362
  }
@@ -497,6 +473,18 @@ function registerCart(program, makeClient2, loadConfig2) {
497
473
  function supportedCountries(region) {
498
474
  return (region.countries ?? []).map((c) => (c.iso_2 ?? "").toLowerCase()).filter((c) => c.length > 0);
499
475
  }
476
+ var HEARTBEAT_EVERY_TICKS = 6;
477
+ function heartbeatLine(attempt, opts = {}) {
478
+ const everyTicks = opts.everyTicks ?? HEARTBEAT_EVERY_TICKS;
479
+ if (attempt <= 1 || (attempt - 1) % everyTicks !== 0) {
480
+ return null;
481
+ }
482
+ const intervalMs = opts.intervalMs ?? 5e3;
483
+ const elapsedS = Math.round((attempt - 1) * intervalMs / 1e3);
484
+ return working(
485
+ `still waiting for the browser hand-off, ${elapsedS}s in. ctrl-c is safe; resume with bt checkout --resume`
486
+ );
487
+ }
500
488
  async function runCheckout(client, config, args, deps) {
501
489
  const cartId = config.cart_id;
502
490
  if (!cartId) {
@@ -562,7 +550,9 @@ async function runCheckout(client, config, args, deps) {
562
550
  what: "couldn't attach a shipping method to your cart.",
563
551
  fix: "this region may not have shipping configured yet \u2014 try again shortly, or re-stage."
564
552
  })
565
- ]
553
+ ],
554
+ exitCode: EXIT.GENERIC
555
+ // fail-closed, no order placed → non-zero (matches the shown exit 1)
566
556
  };
567
557
  }
568
558
  const itemCount = cart.items.reduce((n, li) => n + (li.quantity ?? 1), 0);
@@ -616,7 +606,9 @@ async function runCheckout(client, config, args, deps) {
616
606
  link: checkout_url
617
607
  })
618
608
  ],
619
- save
609
+ save,
610
+ exitCode: EXIT.TEMPFAIL
611
+ // lost the connection mid-poll → not confirmed → non-zero (75)
620
612
  };
621
613
  }
622
614
  const lines = [];
@@ -648,7 +640,7 @@ async function runCheckout(client, config, args, deps) {
648
640
  warn("your stage expired before checkout completed. nothing was charged.")
649
641
  );
650
642
  lines.push(" re-stage \u2192 bt stage <slug> then bt checkout");
651
- return { lines, save, json: args.json ? { status: "expired" } : void 0 };
643
+ return { lines, save, json: args.json ? { status: "expired" } : void 0, exitCode: EXIT.TEMPFAIL };
652
644
  }
653
645
  lines.push("");
654
646
  lines.push(
@@ -660,7 +652,13 @@ async function runCheckout(client, config, args, deps) {
660
652
  link: checkout_url
661
653
  })
662
654
  );
663
- return { lines, save, json: args.json ? { status: "timeout", checkout_url } : void 0 };
655
+ return {
656
+ lines,
657
+ save,
658
+ json: args.json ? { status: "timeout", checkout_url } : void 0,
659
+ exitCode: EXIT.TEMPFAIL
660
+ // handoff timed out → not placed → non-zero (matches the shown exit 75)
661
+ };
664
662
  }
665
663
  async function resolveDisplayId(client, orderId, email) {
666
664
  if (!orderId) {
@@ -687,11 +685,13 @@ function maskEmail(email) {
687
685
  function registerCheckout(program, makeClient2, loadConfig2) {
688
686
  const action = async (opts) => {
689
687
  const { json, brand } = resolveOutput(program);
690
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
688
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
691
689
  const { openCheckout } = await import("./browser-EO5EQUXQ.js");
692
690
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
693
691
  const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
694
692
  const client = makeClient2();
693
+ const print = json ? () => {
694
+ } : (line) => process.stdout.write(line + "\n");
695
695
  const res = await runCheckout(
696
696
  client,
697
697
  loadConfig2(),
@@ -699,11 +699,16 @@ function registerCheckout(program, makeClient2, loadConfig2) {
699
699
  {
700
700
  prompts: makePrompts2(),
701
701
  open: (url) => openCheckout(url),
702
- poll: (sessionId) => pollUntilPlaced(client, sessionId),
702
+ // Heartbeat the browser-hop wait so the terminal never looks hung (a dim, calm line every
703
+ // ~30s). Suppressed under --json (print is a no-op there).
704
+ poll: (sessionId) => pollUntilPlaced(client, sessionId, {
705
+ onTick: (attempt) => {
706
+ const line = heartbeatLine(attempt);
707
+ if (line) print(dim(brand, line));
708
+ }
709
+ }),
703
710
  saveConfig: saveConfig2,
704
- // In --json mode the review is suppressed (machine output only); otherwise print eagerly.
705
- print: json ? () => {
706
- } : (line) => process.stdout.write(line + "\n")
711
+ print
707
712
  }
708
713
  );
709
714
  if (res.save && Object.keys(res.save).length > 0) {
@@ -1424,8 +1429,8 @@ function runInit(args) {
1424
1429
  " buy heavyweight pieces from your terminal. zero setup \u2014 you're already pointed at the live store.",
1425
1430
  "",
1426
1431
  " the happy path:",
1427
- " bt browse # the catalog (arrow keys to pick)",
1428
- " bt stage <handle> --size L # add to your cart",
1432
+ " bt browse # the catalog, numbered",
1433
+ " bt stage <n|slug> --size L # add to your cart (a browse number, or a slug)",
1429
1434
  " bt checkout # pay in your browser \u2014 cards never touch the terminal",
1430
1435
  "",
1431
1436
  " optional:",
@@ -1467,24 +1472,34 @@ async function runBuy(client, config, args, deps) {
1467
1472
  return {
1468
1473
  lines: [...staged.lines, "", ...checked.lines],
1469
1474
  save: { ...staged.save ?? {}, ...checked.save ?? {} },
1470
- json: args.json ? { cart_id: cartId, checkout: checked.json ?? null } : void 0
1475
+ json: args.json ? { cart_id: cartId, checkout: checked.json ?? null } : void 0,
1476
+ // forward the checkout outcome's exit code — bt buy runs the SAME money-path poll as bt
1477
+ // checkout, so a timed-out/failed composed checkout must not report success (d10).
1478
+ exitCode: checked.exitCode
1471
1479
  };
1472
1480
  }
1473
1481
  function registerBuy(program, makeClient2, loadConfig2) {
1474
- program.command("buy").argument("<handle>", "the product handle/slug").option("--size <size>", "garment size").option("--colour <colour>", "colour, e.g. white / black").description("stage + checkout in one \u2014 reserve, then open browser checkout. cards never touch the terminal.").action(async (handle, opts) => {
1482
+ program.command("buy").argument("<handle>", "the design: a slug, a bt browse number (3), or a fuzzy hint (termnl)").option("--size <size>", "garment size").option("--colour <colour>", "colour, e.g. white / black").description("stage + checkout in one \u2014 reserve, then open browser checkout. cards never touch the terminal.").action(async (handle, opts) => {
1475
1483
  const { json, brand } = resolveOutput(program);
1476
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
1484
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1477
1485
  const { openCheckout } = await import("./browser-EO5EQUXQ.js");
1478
1486
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
1479
1487
  const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
1480
1488
  const client = makeClient2();
1489
+ const print = json ? () => {
1490
+ } : (line) => process.stdout.write(line + "\n");
1481
1491
  const res = await runBuy(client, loadConfig2(), { handle, size: opts.size, colour: opts.colour, json, brand }, {
1482
1492
  prompts: makePrompts2(),
1483
1493
  open: (url) => openCheckout(url),
1484
- poll: (sessionId) => pollUntilPlaced(client, sessionId),
1494
+ // Same heartbeat as bt checkout — bt buy's browser-hop wait is the identical 20-min poll.
1495
+ poll: (sessionId) => pollUntilPlaced(client, sessionId, {
1496
+ onTick: (attempt) => {
1497
+ const line = heartbeatLine(attempt);
1498
+ if (line) print(dim(brand, line));
1499
+ }
1500
+ }),
1485
1501
  saveConfig: saveConfig2,
1486
- print: json ? () => {
1487
- } : (line) => process.stdout.write(line + "\n")
1502
+ print
1488
1503
  });
1489
1504
  if (res.save && Object.keys(res.save).length > 0) {
1490
1505
  saveConfig2(res.save);
@@ -1630,7 +1645,7 @@ function registerReport(program, makeClient2) {
1630
1645
  const gOpts = program.opts();
1631
1646
  const json = Boolean(gOpts.json);
1632
1647
  const debug = process.argv.includes("--debug") || process.argv.includes("--verbose");
1633
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
1648
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1634
1649
  const snapshot = gatherDoctorSnapshot({ color: gOpts.color });
1635
1650
  const res = await runReport(
1636
1651
  makeClient2(),
@@ -1641,6 +1656,43 @@ function registerReport(program, makeClient2) {
1641
1656
  });
1642
1657
  }
1643
1658
 
1659
+ // src/commands/blame.ts
1660
+ async function runBlame(client, config, args) {
1661
+ const slug = (args.handle ?? "").trim();
1662
+ if (!slug) {
1663
+ return { lines: blame().split("\n") };
1664
+ }
1665
+ const region = await client.resolveRegion(config.region);
1666
+ const picked = await resolveProduct(client, region, slug);
1667
+ const product = picked.product;
1668
+ if (!product) {
1669
+ return { lines: blame(slug).split("\n") };
1670
+ }
1671
+ const { handle } = displayName(product);
1672
+ const lore = productLore(product);
1673
+ const lines = [];
1674
+ if (picked.fuzzy && handle !== slug) {
1675
+ lines.push(working(`matched "${handle}" from "${slug}"`));
1676
+ }
1677
+ if (!lore.text) {
1678
+ lines.push(...blame(handle).split("\n"));
1679
+ return { lines, json: args.json ? { handle, lore: null } : void 0 };
1680
+ }
1681
+ lines.push(`blame ${handle}`);
1682
+ lines.push(` ${lore.text}`);
1683
+ for (const s of lore.sources) {
1684
+ lines.push(` source ${s.label} (${s.url})`);
1685
+ }
1686
+ return { lines, json: args.json ? { handle, lore } : void 0 };
1687
+ }
1688
+ function registerBlame(program, makeClient2, loadConfig2) {
1689
+ program.command("blame", { hidden: true }).argument("[slug]", "a design \u2014 a slug, a bt browse number, or a fuzzy hint").description("who's responsible for a design \u2014 its lore + sources (git blame, for motifs).").action(async (slug) => {
1690
+ const { json, brand } = resolveOutput(program);
1691
+ const res = await runBlame(makeClient2(), loadConfig2(), { handle: slug, json, brand });
1692
+ printResult(res, json);
1693
+ });
1694
+ }
1695
+
1644
1696
  // src/bin.ts
1645
1697
  function makeClient() {
1646
1698
  const { baseUrl, publishableKey } = resolveClientConfig();
@@ -1657,7 +1709,7 @@ function buildProgram() {
1657
1709
  const program = new Command();
1658
1710
  program.name("bt").description(
1659
1711
  "the binary & thread terminal store. techno-minimalist clothing & lifestyle, bought with the ceremony of a deploy.\nprimary invocation: npx @binaryandthread/cli (a bare `bt` may collide with another binary)"
1660
- ).version(versionLine(VERSION), "-v, --version", "print the version").option("--json", "machine-readable output", false).option("--no-color", "disable ANSI colour + inline images (also honours NO_COLOR)").option("--debug, --verbose", "trace requests to stderr (redacted) \u2014 for bug reports (d15)");
1712
+ ).version(versionLine(VERSION), "-v, --version", "print the version").option("--json", "machine-readable output", false).option("--no-color", "disable ANSI colour + inline images (also honours NO_COLOR)").option("--debug, --verbose", "trace requests to stderr (redacted) \u2014 for bug reports (d15)").showSuggestionAfterError();
1661
1713
  program.addHelpText("beforeAll", "\n bt \u2014 the binary & thread terminal store. new here? \u2192 bt init\n");
1662
1714
  program.command("brew").argument("[what]", "what to brew").description("we make clothing, not coffee.").action(() => {
1663
1715
  process.stdout.write(teapot() + "\n");
@@ -1696,6 +1748,7 @@ function registerCommands(program) {
1696
1748
  registerToken(program);
1697
1749
  registerDoctor(program);
1698
1750
  registerReport(program, makeClient);
1751
+ registerBlame(program, makeClient, loadConfig);
1699
1752
  registerCart(program, makeClient, loadConfig);
1700
1753
  registerCheckout(program, makeClient, loadConfig);
1701
1754
  registerOrder(program, makeClient, loadConfig);
@@ -1731,7 +1784,11 @@ function registerEggs(program) {
1731
1784
  program.command("ship", { hidden: true }).argument("[what]").action(() => print(shipIt()));
1732
1785
  program.command("rm", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(rmWardrobe()));
1733
1786
  program.command("force", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(force()));
1734
- program.command("blame", { hidden: true }).argument("[slug]", "a design slug to blame").description("who's responsible for all this \u2014 provenance + lore.").action((slug) => print(blame(slug)));
1787
+ program.command("clone", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitClone()));
1788
+ program.command("stash", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitStash()));
1789
+ program.command("push", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitPush()));
1790
+ program.command("pull", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitPull()));
1791
+ program.command("reflog", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitReflog()));
1735
1792
  program.command("fortune").description("a coding fortune. real aphorisms only.").action(() => {
1736
1793
  const seed = Math.floor(Date.now() / 1e3) % FORTUNES.length;
1737
1794
  print(fortune(seed));