@binaryandthread/cli 0.1.2 → 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-WYCZ6BPV.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.2";
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,48 +197,18 @@ 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 };
200
+ // src/is-main.ts
201
+ import { realpathSync } from "fs";
202
+ import { fileURLToPath } from "url";
203
+ function isMainModule(argv1, moduleUrl, realpath = realpathSync) {
204
+ if (typeof argv1 !== "string" || argv1.length === 0) return false;
205
+ try {
206
+ const invoked = realpath(argv1);
207
+ const self = realpath(fileURLToPath(moduleUrl));
208
+ return invoked === self;
209
+ } catch {
210
+ return false;
223
211
  }
224
- return { out: null, err: `\u2717 ${info.message}`, code: info.code };
225
212
  }
226
213
 
227
214
  // src/keychain.ts
@@ -298,19 +285,20 @@ function deleteToken(deps = {}) {
298
285
  }
299
286
 
300
287
  // src/commands/browse.ts
301
- function productRow(p, stage, style) {
288
+ function productRow(p, index, stage, style) {
302
289
  const { amount, currency } = firstPrice(p);
303
290
  const { title, handle } = displayName(p);
304
291
  const colours = colourMarker(productColours(p));
292
+ const ref = dim(style, `[${index + 1}]`.padEnd(4));
305
293
  const parts = [
306
294
  title.padEnd(22),
307
295
  cyan(style, fmtPrice(amount, currency).padEnd(11)),
308
- dim(style, handle.padEnd(34)),
296
+ dim(style, handle.padEnd(30)),
309
297
  stageMarker(stage)
310
298
  ];
311
- const row = parts.join(" ").trimEnd();
299
+ const row = `${ref} ${parts.join(" ")}`.trimEnd();
312
300
  return colours ? `${row}
313
- ${dim(style, colours)}` : row;
301
+ ${dim(style, colours)}` : row;
314
302
  }
315
303
  async function runBrowse(client, config, args) {
316
304
  const style = brandOf(args);
@@ -330,9 +318,9 @@ async function runBrowse(client, config, args) {
330
318
  lines.push(" peek at next \u2192 bt browse --branch=next");
331
319
  } else {
332
320
  lines.push(header(style, "bt browse \u2014 in stock"));
333
- for (const { p, stage } of rows) {
334
- lines.push(productRow(p, stage, style));
335
- }
321
+ rows.forEach(({ p, stage }, i) => {
322
+ lines.push(productRow(p, i, stage, style));
323
+ });
336
324
  for (const d of drops) {
337
325
  const dt = (d.title ?? d.handle ?? d.id).toString();
338
326
  lines.push(
@@ -347,7 +335,7 @@ async function runBrowse(client, config, args) {
347
335
  if (drops.length > 0) {
348
336
  lines.push(" on next, before the web \u2192 bt drops");
349
337
  }
350
- 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");
351
339
  }
352
340
  return {
353
341
  lines,
@@ -363,10 +351,10 @@ async function runBrowse(client, config, args) {
363
351
  };
364
352
  }
365
353
  function registerBrowse(program, makeClient2, loadConfig2) {
366
- 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) => {
367
355
  const { json, brand } = resolveOutput(program);
368
- if (!json && !opts.plain && brand.color && process.stdout.isTTY) {
369
- const { runInteractiveBrowse } = await import("./interactive-DYY27KBC.js");
356
+ if (opts.pick && !json && brand.color && process.stdout.isTTY && process.stdin.isTTY) {
357
+ const { runInteractiveBrowse } = await import("./interactive-GY4AK7D2.js");
370
358
  const handled = await runInteractiveBrowse(makeClient2(), loadConfig2(), { branch: opts.branch, brand });
371
359
  if (handled) return;
372
360
  }
@@ -483,6 +471,18 @@ function registerCart(program, makeClient2, loadConfig2) {
483
471
  function supportedCountries(region) {
484
472
  return (region.countries ?? []).map((c) => (c.iso_2 ?? "").toLowerCase()).filter((c) => c.length > 0);
485
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
+ }
486
486
  async function runCheckout(client, config, args, deps) {
487
487
  const cartId = config.cart_id;
488
488
  if (!cartId) {
@@ -548,7 +548,9 @@ async function runCheckout(client, config, args, deps) {
548
548
  what: "couldn't attach a shipping method to your cart.",
549
549
  fix: "this region may not have shipping configured yet \u2014 try again shortly, or re-stage."
550
550
  })
551
- ]
551
+ ],
552
+ exitCode: EXIT.GENERIC
553
+ // fail-closed, no order placed → non-zero (matches the shown exit 1)
552
554
  };
553
555
  }
554
556
  const itemCount = cart.items.reduce((n, li) => n + (li.quantity ?? 1), 0);
@@ -602,7 +604,9 @@ async function runCheckout(client, config, args, deps) {
602
604
  link: checkout_url
603
605
  })
604
606
  ],
605
- save
607
+ save,
608
+ exitCode: EXIT.TEMPFAIL
609
+ // lost the connection mid-poll → not confirmed → non-zero (75)
606
610
  };
607
611
  }
608
612
  const lines = [];
@@ -634,7 +638,7 @@ async function runCheckout(client, config, args, deps) {
634
638
  warn("your stage expired before checkout completed. nothing was charged.")
635
639
  );
636
640
  lines.push(" re-stage \u2192 bt stage <slug> then bt checkout");
637
- return { lines, save, json: args.json ? { status: "expired" } : void 0 };
641
+ return { lines, save, json: args.json ? { status: "expired" } : void 0, exitCode: EXIT.TEMPFAIL };
638
642
  }
639
643
  lines.push("");
640
644
  lines.push(
@@ -646,7 +650,13 @@ async function runCheckout(client, config, args, deps) {
646
650
  link: checkout_url
647
651
  })
648
652
  );
649
- 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
+ };
650
660
  }
651
661
  async function resolveDisplayId(client, orderId, email) {
652
662
  if (!orderId) {
@@ -673,11 +683,13 @@ function maskEmail(email) {
673
683
  function registerCheckout(program, makeClient2, loadConfig2) {
674
684
  const action = async (opts) => {
675
685
  const { json, brand } = resolveOutput(program);
676
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
686
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
677
687
  const { openCheckout } = await import("./browser-EO5EQUXQ.js");
678
688
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
679
689
  const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
680
690
  const client = makeClient2();
691
+ const print = json ? () => {
692
+ } : (line) => process.stdout.write(line + "\n");
681
693
  const res = await runCheckout(
682
694
  client,
683
695
  loadConfig2(),
@@ -685,11 +697,16 @@ function registerCheckout(program, makeClient2, loadConfig2) {
685
697
  {
686
698
  prompts: makePrompts2(),
687
699
  open: (url) => openCheckout(url),
688
- 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
+ }),
689
708
  saveConfig: saveConfig2,
690
- // In --json mode the review is suppressed (machine output only); otherwise print eagerly.
691
- print: json ? () => {
692
- } : (line) => process.stdout.write(line + "\n")
709
+ print
693
710
  }
694
711
  );
695
712
  if (res.save && Object.keys(res.save).length > 0) {
@@ -1410,8 +1427,8 @@ function runInit(args) {
1410
1427
  " buy heavyweight pieces from your terminal. zero setup \u2014 you're already pointed at the live store.",
1411
1428
  "",
1412
1429
  " the happy path:",
1413
- " bt browse # the catalog (arrow keys to pick)",
1414
- " 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)",
1415
1432
  " bt checkout # pay in your browser \u2014 cards never touch the terminal",
1416
1433
  "",
1417
1434
  " optional:",
@@ -1453,24 +1470,34 @@ async function runBuy(client, config, args, deps) {
1453
1470
  return {
1454
1471
  lines: [...staged.lines, "", ...checked.lines],
1455
1472
  save: { ...staged.save ?? {}, ...checked.save ?? {} },
1456
- 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
1457
1477
  };
1458
1478
  }
1459
1479
  function registerBuy(program, makeClient2, loadConfig2) {
1460
- 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) => {
1461
1481
  const { json, brand } = resolveOutput(program);
1462
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
1482
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1463
1483
  const { openCheckout } = await import("./browser-EO5EQUXQ.js");
1464
1484
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
1465
1485
  const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
1466
1486
  const client = makeClient2();
1487
+ const print = json ? () => {
1488
+ } : (line) => process.stdout.write(line + "\n");
1467
1489
  const res = await runBuy(client, loadConfig2(), { handle, size: opts.size, colour: opts.colour, json, brand }, {
1468
1490
  prompts: makePrompts2(),
1469
1491
  open: (url) => openCheckout(url),
1470
- 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
+ }),
1471
1499
  saveConfig: saveConfig2,
1472
- print: json ? () => {
1473
- } : (line) => process.stdout.write(line + "\n")
1500
+ print
1474
1501
  });
1475
1502
  if (res.save && Object.keys(res.save).length > 0) {
1476
1503
  saveConfig2(res.save);
@@ -1616,7 +1643,7 @@ function registerReport(program, makeClient2) {
1616
1643
  const gOpts = program.opts();
1617
1644
  const json = Boolean(gOpts.json);
1618
1645
  const debug = process.argv.includes("--debug") || process.argv.includes("--verbose");
1619
- const { makePrompts: makePrompts2 } = await import("./prompts-GTC5VYM6.js");
1646
+ const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1620
1647
  const snapshot = gatherDoctorSnapshot({ color: gOpts.color });
1621
1648
  const res = await runReport(
1622
1649
  makeClient2(),
@@ -1643,7 +1670,7 @@ function buildProgram() {
1643
1670
  const program = new Command();
1644
1671
  program.name("bt").description(
1645
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)"
1646
- ).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();
1647
1674
  program.addHelpText("beforeAll", "\n bt \u2014 the binary & thread terminal store. new here? \u2192 bt init\n");
1648
1675
  program.command("brew").argument("[what]", "what to brew").description("we make clothing, not coffee.").action(() => {
1649
1676
  process.stdout.write(teapot() + "\n");
@@ -1717,6 +1744,11 @@ function registerEggs(program) {
1717
1744
  program.command("ship", { hidden: true }).argument("[what]").action(() => print(shipIt()));
1718
1745
  program.command("rm", { hidden: true }).allowUnknownOption().argument("[args...]").action(() => print(rmWardrobe()));
1719
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()));
1720
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)));
1721
1753
  program.command("fortune").description("a coding fortune. real aphorisms only.").action(() => {
1722
1754
  const seed = Math.floor(Date.now() / 1e3) % FORTUNES.length;
@@ -1724,14 +1756,7 @@ function registerEggs(program) {
1724
1756
  });
1725
1757
  program.command("man", { hidden: true }).description("the bt man page (also installed for the system man(1)).").action(() => print(manPage(VERSION).join("\n")));
1726
1758
  }
1727
- var isMain = (() => {
1728
- try {
1729
- return typeof process.argv[1] === "string" && (process.argv[1].endsWith("bin.js") || process.argv[1].endsWith("bin.ts"));
1730
- } catch {
1731
- return false;
1732
- }
1733
- })();
1734
- if (isMain) {
1759
+ if (isMainModule(process.argv[1], import.meta.url)) {
1735
1760
  buildProgram().parseAsync(process.argv).catch((e) => {
1736
1761
  const json = process.argv.includes("--json");
1737
1762
  const { out, err, code } = formatFatal(e, json);