@binaryandthread/cli 0.1.3 → 0.1.4

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,6 +28,7 @@ import {
27
28
  firstPrice,
28
29
  fmtPrice,
29
30
  formatCartLine,
31
+ formatFatal,
30
32
  green,
31
33
  header,
32
34
  lineColour,
@@ -44,7 +46,7 @@ import {
44
46
  versionLine,
45
47
  warn,
46
48
  working
47
- } from "./chunk-7FH7BZ3I.js";
49
+ } from "./chunk-V7YRJ67J.js";
48
50
  import {
49
51
  __require
50
52
  } from "./chunk-7OCVIDC7.js";
@@ -73,7 +75,7 @@ function makeLogger(debug, write = (s) => process.stderr.write(s)) {
73
75
  }
74
76
 
75
77
  // src/version.ts
76
- var VERSION = "0.1.3";
78
+ var VERSION = "0.1.4";
77
79
 
78
80
  // src/eggs.ts
79
81
  function teapot() {
@@ -102,6 +104,21 @@ function rmWardrobe() {
102
104
  function force() {
103
105
  return "--force-pushing to prod, are we. there's no --force here \u2014\n a commit to your wardrobe is already that decisive.";
104
106
  }
107
+ function gitClone() {
108
+ return "no clone for a fit \u2014 each one's committed by hand. browse the originals \u2192 bt browse";
109
+ }
110
+ function gitStash() {
111
+ return "that's bt unstage here. your cart holds; nothing's lost.";
112
+ }
113
+ function gitPush() {
114
+ return "no remote for your wardrobe. the only push that ships \u2192 bt commit";
115
+ }
116
+ function gitPull() {
117
+ return "nothing to pull \u2014 the freshest designs land as drops \u2192 bt drops";
118
+ }
119
+ function gitReflog() {
120
+ return "nothing's ever truly lost here. your history \u2192 bt log";
121
+ }
105
122
  var FORTUNES = [
106
123
  '// "there are only two hard things in computer science: cache invalidation and naming things." \u2014 phil karlton',
107
124
  '// "premature optimization is the root of all evil." \u2014 donald knuth',
@@ -180,50 +197,6 @@ function manPage(version) {
180
197
  ];
181
198
  }
182
199
 
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
200
  // src/is-main.ts
228
201
  import { realpathSync } from "fs";
229
202
  import { fileURLToPath } from "url";
@@ -312,19 +285,20 @@ function deleteToken(deps = {}) {
312
285
  }
313
286
 
314
287
  // src/commands/browse.ts
315
- function productRow(p, stage, style) {
288
+ function productRow(p, index, stage, style) {
316
289
  const { amount, currency } = firstPrice(p);
317
290
  const { title, handle } = displayName(p);
318
291
  const colours = colourMarker(productColours(p));
292
+ const ref = dim(style, `[${index + 1}]`.padEnd(4));
319
293
  const parts = [
320
294
  title.padEnd(22),
321
295
  cyan(style, fmtPrice(amount, currency).padEnd(11)),
322
- dim(style, handle.padEnd(34)),
296
+ dim(style, handle.padEnd(30)),
323
297
  stageMarker(stage)
324
298
  ];
325
- const row = parts.join(" ").trimEnd();
299
+ const row = `${ref} ${parts.join(" ")}`.trimEnd();
326
300
  return colours ? `${row}
327
- ${dim(style, colours)}` : row;
301
+ ${dim(style, colours)}` : row;
328
302
  }
329
303
  async function runBrowse(client, config, args) {
330
304
  const style = brandOf(args);
@@ -344,9 +318,9 @@ async function runBrowse(client, config, args) {
344
318
  lines.push(" peek at next \u2192 bt browse --branch=next");
345
319
  } else {
346
320
  lines.push(header(style, "bt browse \u2014 in stock"));
347
- for (const { p, stage } of rows) {
348
- lines.push(productRow(p, stage, style));
349
- }
321
+ rows.forEach(({ p, stage }, i) => {
322
+ lines.push(productRow(p, i, stage, style));
323
+ });
350
324
  for (const d of drops) {
351
325
  const dt = (d.title ?? d.handle ?? d.id).toString();
352
326
  lines.push(
@@ -361,7 +335,7 @@ async function runBrowse(client, config, args) {
361
335
  if (drops.length > 0) {
362
336
  lines.push(" on next, before the web \u2192 bt drops");
363
337
  }
364
- lines.push(" view one \u2192 bt view <handle> \xB7 stage one \u2192 bt stage <handle> --size L");
338
+ lines.push(" view one \u2192 bt view <n|slug> \xB7 stage one \u2192 bt stage <n|slug> --size L");
365
339
  }
366
340
  return {
367
341
  lines,
@@ -377,10 +351,10 @@ async function runBrowse(client, config, args) {
377
351
  };
378
352
  }
379
353
  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) => {
354
+ 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
355
  const { json, brand } = resolveOutput(program);
382
- if (!json && !opts.plain && brand.color && process.stdout.isTTY) {
383
- const { runInteractiveBrowse } = await import("./interactive-DZ67P6UX.js");
356
+ if (opts.pick && !json && brand.color && process.stdout.isTTY && process.stdin.isTTY) {
357
+ const { runInteractiveBrowse } = await import("./interactive-GY4AK7D2.js");
384
358
  const handled = await runInteractiveBrowse(makeClient2(), loadConfig2(), { branch: opts.branch, brand });
385
359
  if (handled) return;
386
360
  }
@@ -497,6 +471,18 @@ function registerCart(program, makeClient2, loadConfig2) {
497
471
  function supportedCountries(region) {
498
472
  return (region.countries ?? []).map((c) => (c.iso_2 ?? "").toLowerCase()).filter((c) => c.length > 0);
499
473
  }
474
+ var HEARTBEAT_EVERY_TICKS = 6;
475
+ function heartbeatLine(attempt, opts = {}) {
476
+ const everyTicks = opts.everyTicks ?? HEARTBEAT_EVERY_TICKS;
477
+ if (attempt <= 1 || (attempt - 1) % everyTicks !== 0) {
478
+ return null;
479
+ }
480
+ const intervalMs = opts.intervalMs ?? 5e3;
481
+ const elapsedS = Math.round((attempt - 1) * intervalMs / 1e3);
482
+ return working(
483
+ `still waiting for the browser hand-off, ${elapsedS}s in. ctrl-c is safe; resume with bt checkout --resume`
484
+ );
485
+ }
500
486
  async function runCheckout(client, config, args, deps) {
501
487
  const cartId = config.cart_id;
502
488
  if (!cartId) {
@@ -562,7 +548,9 @@ async function runCheckout(client, config, args, deps) {
562
548
  what: "couldn't attach a shipping method to your cart.",
563
549
  fix: "this region may not have shipping configured yet \u2014 try again shortly, or re-stage."
564
550
  })
565
- ]
551
+ ],
552
+ exitCode: EXIT.GENERIC
553
+ // fail-closed, no order placed → non-zero (matches the shown exit 1)
566
554
  };
567
555
  }
568
556
  const itemCount = cart.items.reduce((n, li) => n + (li.quantity ?? 1), 0);
@@ -616,7 +604,9 @@ async function runCheckout(client, config, args, deps) {
616
604
  link: checkout_url
617
605
  })
618
606
  ],
619
- save
607
+ save,
608
+ exitCode: EXIT.TEMPFAIL
609
+ // lost the connection mid-poll → not confirmed → non-zero (75)
620
610
  };
621
611
  }
622
612
  const lines = [];
@@ -648,7 +638,7 @@ async function runCheckout(client, config, args, deps) {
648
638
  warn("your stage expired before checkout completed. nothing was charged.")
649
639
  );
650
640
  lines.push(" re-stage \u2192 bt stage <slug> then bt checkout");
651
- return { lines, save, json: args.json ? { status: "expired" } : void 0 };
641
+ return { lines, save, json: args.json ? { status: "expired" } : void 0, exitCode: EXIT.TEMPFAIL };
652
642
  }
653
643
  lines.push("");
654
644
  lines.push(
@@ -660,7 +650,13 @@ async function runCheckout(client, config, args, deps) {
660
650
  link: checkout_url
661
651
  })
662
652
  );
663
- return { lines, save, json: args.json ? { status: "timeout", checkout_url } : void 0 };
653
+ return {
654
+ lines,
655
+ save,
656
+ json: args.json ? { status: "timeout", checkout_url } : void 0,
657
+ exitCode: EXIT.TEMPFAIL
658
+ // handoff timed out → not placed → non-zero (matches the shown exit 75)
659
+ };
664
660
  }
665
661
  async function resolveDisplayId(client, orderId, email) {
666
662
  if (!orderId) {
@@ -687,11 +683,13 @@ function maskEmail(email) {
687
683
  function registerCheckout(program, makeClient2, loadConfig2) {
688
684
  const action = async (opts) => {
689
685
  const { json, brand } = resolveOutput(program);
690
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
686
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
691
687
  const { openCheckout } = await import("./browser-EO5EQUXQ.js");
692
688
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
693
689
  const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
694
690
  const client = makeClient2();
691
+ const print = json ? () => {
692
+ } : (line) => process.stdout.write(line + "\n");
695
693
  const res = await runCheckout(
696
694
  client,
697
695
  loadConfig2(),
@@ -699,11 +697,16 @@ function registerCheckout(program, makeClient2, loadConfig2) {
699
697
  {
700
698
  prompts: makePrompts2(),
701
699
  open: (url) => openCheckout(url),
702
- poll: (sessionId) => pollUntilPlaced(client, sessionId),
700
+ // Heartbeat the browser-hop wait so the terminal never looks hung (a dim, calm line every
701
+ // ~30s). Suppressed under --json (print is a no-op there).
702
+ poll: (sessionId) => pollUntilPlaced(client, sessionId, {
703
+ onTick: (attempt) => {
704
+ const line = heartbeatLine(attempt);
705
+ if (line) print(dim(brand, line));
706
+ }
707
+ }),
703
708
  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")
709
+ print
707
710
  }
708
711
  );
709
712
  if (res.save && Object.keys(res.save).length > 0) {
@@ -1424,8 +1427,8 @@ function runInit(args) {
1424
1427
  " buy heavyweight pieces from your terminal. zero setup \u2014 you're already pointed at the live store.",
1425
1428
  "",
1426
1429
  " the happy path:",
1427
- " bt browse # the catalog (arrow keys to pick)",
1428
- " bt stage <handle> --size L # add to your cart",
1430
+ " bt browse # the catalog, numbered",
1431
+ " bt stage <n|slug> --size L # add to your cart (a browse number, or a slug)",
1429
1432
  " bt checkout # pay in your browser \u2014 cards never touch the terminal",
1430
1433
  "",
1431
1434
  " optional:",
@@ -1467,24 +1470,34 @@ async function runBuy(client, config, args, deps) {
1467
1470
  return {
1468
1471
  lines: [...staged.lines, "", ...checked.lines],
1469
1472
  save: { ...staged.save ?? {}, ...checked.save ?? {} },
1470
- json: args.json ? { cart_id: cartId, checkout: checked.json ?? null } : void 0
1473
+ json: args.json ? { cart_id: cartId, checkout: checked.json ?? null } : void 0,
1474
+ // forward the checkout outcome's exit code — bt buy runs the SAME money-path poll as bt
1475
+ // checkout, so a timed-out/failed composed checkout must not report success (d10).
1476
+ exitCode: checked.exitCode
1471
1477
  };
1472
1478
  }
1473
1479
  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) => {
1480
+ 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
1481
  const { json, brand } = resolveOutput(program);
1476
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
1482
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1477
1483
  const { openCheckout } = await import("./browser-EO5EQUXQ.js");
1478
1484
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
1479
1485
  const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
1480
1486
  const client = makeClient2();
1487
+ const print = json ? () => {
1488
+ } : (line) => process.stdout.write(line + "\n");
1481
1489
  const res = await runBuy(client, loadConfig2(), { handle, size: opts.size, colour: opts.colour, json, brand }, {
1482
1490
  prompts: makePrompts2(),
1483
1491
  open: (url) => openCheckout(url),
1484
- poll: (sessionId) => pollUntilPlaced(client, sessionId),
1492
+ // Same heartbeat as bt checkout — bt buy's browser-hop wait is the identical 20-min poll.
1493
+ poll: (sessionId) => pollUntilPlaced(client, sessionId, {
1494
+ onTick: (attempt) => {
1495
+ const line = heartbeatLine(attempt);
1496
+ if (line) print(dim(brand, line));
1497
+ }
1498
+ }),
1485
1499
  saveConfig: saveConfig2,
1486
- print: json ? () => {
1487
- } : (line) => process.stdout.write(line + "\n")
1500
+ print
1488
1501
  });
1489
1502
  if (res.save && Object.keys(res.save).length > 0) {
1490
1503
  saveConfig2(res.save);
@@ -1630,7 +1643,7 @@ function registerReport(program, makeClient2) {
1630
1643
  const gOpts = program.opts();
1631
1644
  const json = Boolean(gOpts.json);
1632
1645
  const debug = process.argv.includes("--debug") || process.argv.includes("--verbose");
1633
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
1646
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1634
1647
  const snapshot = gatherDoctorSnapshot({ color: gOpts.color });
1635
1648
  const res = await runReport(
1636
1649
  makeClient2(),
@@ -1657,7 +1670,7 @@ function buildProgram() {
1657
1670
  const program = new Command();
1658
1671
  program.name("bt").description(
1659
1672
  "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)");
1673
+ ).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
1674
  program.addHelpText("beforeAll", "\n bt \u2014 the binary & thread terminal store. new here? \u2192 bt init\n");
1662
1675
  program.command("brew").argument("[what]", "what to brew").description("we make clothing, not coffee.").action(() => {
1663
1676
  process.stdout.write(teapot() + "\n");
@@ -1731,6 +1744,11 @@ function registerEggs(program) {
1731
1744
  program.command("ship", { hidden: true }).argument("[what]").action(() => print(shipIt()));
1732
1745
  program.command("rm", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(rmWardrobe()));
1733
1746
  program.command("force", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(force()));
1747
+ program.command("clone", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitClone()));
1748
+ program.command("stash", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitStash()));
1749
+ program.command("push", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitPush()));
1750
+ program.command("pull", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitPull()));
1751
+ program.command("reflog", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(gitReflog()));
1734
1752
  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)));
1735
1753
  program.command("fortune").description("a coding fortune. real aphorisms only.").action(() => {
1736
1754
  const seed = Math.floor(Date.now() / 1e3) % FORTUNES.length;