@binaryandthread/cli 0.1.9 → 0.2.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.
- package/README.md +12 -3
- package/dist/bin.js +321 -98
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-RGXH3EZ6.js → chunk-NJ3DW5AX.js} +113 -62
- package/dist/chunk-NJ3DW5AX.js.map +1 -0
- package/dist/{interactive-2YROZBIN.js → interactive-5PBJCENZ.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-RGXH3EZ6.js.map +0 -1
- /package/dist/{interactive-2YROZBIN.js.map → interactive-5PBJCENZ.js.map} +0 -0
package/dist/bin.js
CHANGED
|
@@ -26,18 +26,21 @@ import {
|
|
|
26
26
|
displayName,
|
|
27
27
|
empty,
|
|
28
28
|
fail,
|
|
29
|
+
failResult,
|
|
29
30
|
firstPrice,
|
|
30
31
|
fmtPrice,
|
|
31
32
|
formatCartLine,
|
|
32
33
|
formatFatal,
|
|
33
34
|
green,
|
|
34
35
|
header,
|
|
36
|
+
isCartTaxInclusive,
|
|
35
37
|
lineColour,
|
|
36
38
|
ok,
|
|
37
39
|
printResult,
|
|
38
40
|
productColours,
|
|
39
41
|
productLore,
|
|
40
42
|
redact,
|
|
43
|
+
regionLine,
|
|
41
44
|
registerStage,
|
|
42
45
|
registerView,
|
|
43
46
|
resolveLineItemRef,
|
|
@@ -46,10 +49,11 @@ import {
|
|
|
46
49
|
rule,
|
|
47
50
|
runStage,
|
|
48
51
|
stageMarker,
|
|
52
|
+
taxSuffix,
|
|
49
53
|
versionLine,
|
|
50
54
|
warn,
|
|
51
55
|
working
|
|
52
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-NJ3DW5AX.js";
|
|
53
57
|
import {
|
|
54
58
|
__require
|
|
55
59
|
} from "./chunk-7OCVIDC7.js";
|
|
@@ -78,7 +82,7 @@ function makeLogger(debug, write = (s) => process.stderr.write(s)) {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// src/version.ts
|
|
81
|
-
var VERSION = "0.
|
|
85
|
+
var VERSION = "0.2.0";
|
|
82
86
|
|
|
83
87
|
// src/eggs.ts
|
|
84
88
|
function teapot() {
|
|
@@ -371,24 +375,55 @@ function deleteToken(deps = {}) {
|
|
|
371
375
|
}
|
|
372
376
|
|
|
373
377
|
// src/commands/browse.ts
|
|
374
|
-
function
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
378
|
+
function truncate(s, w) {
|
|
379
|
+
if (s.length <= w) return s;
|
|
380
|
+
if (w <= 1) return s.slice(0, Math.max(0, w));
|
|
381
|
+
return s.slice(0, w - 1) + "\u2026";
|
|
382
|
+
}
|
|
383
|
+
function layoutRows(rows, style) {
|
|
384
|
+
const cells = rows.map(({ p, stage }, i) => {
|
|
385
|
+
const { amount, currency } = firstPrice(p);
|
|
386
|
+
const { title, handle } = displayName(p);
|
|
387
|
+
return {
|
|
388
|
+
ref: `[${i + 1}]`,
|
|
389
|
+
title,
|
|
390
|
+
price: fmtPrice(amount, currency),
|
|
391
|
+
handle,
|
|
392
|
+
stage,
|
|
393
|
+
colours: colourMarker(productColours(p))
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
const refW = Math.max(...cells.map((c) => c.ref.length));
|
|
397
|
+
const priceW = Math.max(...cells.map((c) => c.price.length));
|
|
398
|
+
const handleW = Math.max(...cells.map((c) => c.handle.length));
|
|
399
|
+
const maxTitle = Math.max(...cells.map((c) => c.title.length));
|
|
400
|
+
const GAP = 2;
|
|
401
|
+
const STAGE_W = 12;
|
|
402
|
+
const term = process.stdout.columns ?? 0;
|
|
403
|
+
let titleW = maxTitle;
|
|
404
|
+
if (term > 0) {
|
|
405
|
+
const fixed = refW + GAP + priceW + GAP + handleW + GAP + STAGE_W;
|
|
406
|
+
titleW = Math.max(12, Math.min(maxTitle, term - fixed));
|
|
407
|
+
}
|
|
408
|
+
const out = [];
|
|
409
|
+
for (const c of cells) {
|
|
410
|
+
const row = [
|
|
411
|
+
dim(style, c.ref.padEnd(refW)),
|
|
412
|
+
truncate(c.title, titleW).padEnd(titleW),
|
|
413
|
+
cyan(style, c.price.padStart(priceW)),
|
|
414
|
+
// right-align money — reads like a ledger
|
|
415
|
+
dim(style, c.handle.padEnd(handleW)),
|
|
416
|
+
stageMarker(c.stage)
|
|
417
|
+
].join(" ").trimEnd();
|
|
418
|
+
out.push(c.colours ? `${row}
|
|
419
|
+
${dim(style, c.colours)}` : row);
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
388
422
|
}
|
|
389
423
|
async function runBrowse(client, config, args) {
|
|
390
424
|
const style = brandOf(args);
|
|
391
|
-
const
|
|
425
|
+
const rr = await client.resolveRegionResult(config.region);
|
|
426
|
+
const region = rr.region;
|
|
392
427
|
const products = await client.listProducts({ region });
|
|
393
428
|
const rows = products.map((p) => ({
|
|
394
429
|
p,
|
|
@@ -404,9 +439,8 @@ async function runBrowse(client, config, args) {
|
|
|
404
439
|
lines.push(" peek at next \u2192 bt browse --branch=next");
|
|
405
440
|
} else {
|
|
406
441
|
lines.push(header(style, "bt browse \u2014 in stock"));
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
442
|
+
lines.push(regionLine(rr, style));
|
|
443
|
+
lines.push(...layoutRows(rows, style));
|
|
410
444
|
for (const d of drops) {
|
|
411
445
|
const dt = (d.title ?? d.handle ?? d.id).toString();
|
|
412
446
|
lines.push(
|
|
@@ -421,6 +455,10 @@ async function runBrowse(client, config, args) {
|
|
|
421
455
|
if (drops.length > 0) {
|
|
422
456
|
lines.push(" on next, before the web \u2192 bt drops");
|
|
423
457
|
}
|
|
458
|
+
const taxIncl = rows.length > 0 ? firstPrice(rows[0].p).taxInclusive : null;
|
|
459
|
+
if (taxIncl === true) {
|
|
460
|
+
lines.push(dim(style, " prices incl. VAT"));
|
|
461
|
+
}
|
|
424
462
|
lines.push(" view one \u2192 bt view <n|slug> \xB7 stage one \u2192 bt stage <n|slug> --size L");
|
|
425
463
|
}
|
|
426
464
|
return {
|
|
@@ -440,7 +478,7 @@ function registerBrowse(program, makeClient2, loadConfig2) {
|
|
|
440
478
|
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) => {
|
|
441
479
|
const { json, brand } = resolveOutput(program);
|
|
442
480
|
if (opts.pick && !json && brand.color && process.stdout.isTTY && process.stdin.isTTY) {
|
|
443
|
-
const { runInteractiveBrowse } = await import("./interactive-
|
|
481
|
+
const { runInteractiveBrowse } = await import("./interactive-5PBJCENZ.js");
|
|
444
482
|
const handled = await runInteractiveBrowse(makeClient2(), loadConfig2(), { branch: opts.branch, brand });
|
|
445
483
|
if (handled) return;
|
|
446
484
|
}
|
|
@@ -449,6 +487,143 @@ function registerBrowse(program, makeClient2, loadConfig2) {
|
|
|
449
487
|
});
|
|
450
488
|
}
|
|
451
489
|
|
|
490
|
+
// src/commands/region.ts
|
|
491
|
+
var MAX_COUNTRIES_SHOWN = 8;
|
|
492
|
+
function regionRow(r) {
|
|
493
|
+
const ccs = (r.countries ?? []).map((c) => (c.iso_2 ?? "").toLowerCase()).filter(Boolean).sort();
|
|
494
|
+
const shown = ccs.slice(0, MAX_COUNTRIES_SHOWN).join(" \xB7 ");
|
|
495
|
+
const more = ccs.length > MAX_COUNTRIES_SHOWN ? ` \u2026 (${ccs.length})` : "";
|
|
496
|
+
return ` ${r.currency_code.toLowerCase().padEnd(5)} ${shown}${more}`.trimEnd();
|
|
497
|
+
}
|
|
498
|
+
async function runRegion(client, config, args) {
|
|
499
|
+
const style = brandOf(args);
|
|
500
|
+
if (args.cc) {
|
|
501
|
+
const cc = args.cc.trim().toLowerCase();
|
|
502
|
+
const rr2 = await client.resolveRegionResult(cc);
|
|
503
|
+
if (!rr2.matched) {
|
|
504
|
+
return failResult({
|
|
505
|
+
code: EXIT.DATAERR,
|
|
506
|
+
what: `we don't ship to ${cc} yet. nothing was set.`,
|
|
507
|
+
fix: "bt region --list (see where we ship)"
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
lines: [ok(`region \u2192 ${cc} \xB7 prices in ${rr2.region.currency_code}`)],
|
|
512
|
+
save: { region: cc },
|
|
513
|
+
json: args.json ? { region: cc, currency: rr2.region.currency_code } : void 0
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (args.list) {
|
|
517
|
+
const regions = await client.getRegions();
|
|
518
|
+
const lines = [
|
|
519
|
+
dim(style, "where we ship, and what you pay in:"),
|
|
520
|
+
...regions.map(regionRow),
|
|
521
|
+
dim(style, " set yours \u2192 bt region <cc>")
|
|
522
|
+
];
|
|
523
|
+
return {
|
|
524
|
+
lines,
|
|
525
|
+
json: args.json ? { regions: regions.map((r) => ({ currency: r.currency_code, countries: (r.countries ?? []).map((c) => c.iso_2) })) } : void 0
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const rr = await client.resolveRegionResult(config.region);
|
|
529
|
+
return {
|
|
530
|
+
lines: [regionLine(rr, style), dim(style, " change \u2192 bt region <cc> \xB7 all \u2192 bt region --list")],
|
|
531
|
+
json: args.json ? { region: rr.requested ?? null, currency: rr.region.currency_code, matched: rr.matched, home: rr.home } : void 0
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function registerRegion(program, makeClient2, loadConfig2, persist) {
|
|
535
|
+
program.command("region").argument("[cc]", "set your ship-to country (ISO-2, e.g. se / us / de)").option("--list", "list every country we ship to + its currency").description("see (or set) your ship-to country + currency.").action(async (cc, opts) => {
|
|
536
|
+
const { json, brand } = resolveOutput(program);
|
|
537
|
+
const res = await runRegion(makeClient2(), loadConfig2(), { cc, list: opts.list, json, brand });
|
|
538
|
+
if (res.save) {
|
|
539
|
+
persist(res.save);
|
|
540
|
+
}
|
|
541
|
+
printResult(res, json);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/commands/completion.ts
|
|
546
|
+
var COMMANDS = [
|
|
547
|
+
"browse",
|
|
548
|
+
"search",
|
|
549
|
+
"view",
|
|
550
|
+
"stage",
|
|
551
|
+
"buy",
|
|
552
|
+
"cart",
|
|
553
|
+
"unstage",
|
|
554
|
+
"checkout",
|
|
555
|
+
"commit",
|
|
556
|
+
"order",
|
|
557
|
+
"log",
|
|
558
|
+
"status",
|
|
559
|
+
"whoami",
|
|
560
|
+
"region",
|
|
561
|
+
"drops",
|
|
562
|
+
"blame",
|
|
563
|
+
"login",
|
|
564
|
+
"logout",
|
|
565
|
+
"config",
|
|
566
|
+
"token",
|
|
567
|
+
"doctor",
|
|
568
|
+
"report",
|
|
569
|
+
"connect",
|
|
570
|
+
"init",
|
|
571
|
+
"completion"
|
|
572
|
+
];
|
|
573
|
+
function completionScript(shell) {
|
|
574
|
+
const list = COMMANDS.join(" ");
|
|
575
|
+
if (shell === "bash") {
|
|
576
|
+
return [
|
|
577
|
+
'# bt bash completion \u2014 add to ~/.bashrc: eval "$(bt completion bash)"',
|
|
578
|
+
"_bt_completions() {",
|
|
579
|
+
' local cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
580
|
+
` COMPREPLY=( $(compgen -W "${list}" -- "$cur") )`,
|
|
581
|
+
"}",
|
|
582
|
+
"complete -F _bt_completions bt btn",
|
|
583
|
+
""
|
|
584
|
+
].join("\n");
|
|
585
|
+
}
|
|
586
|
+
if (shell === "zsh") {
|
|
587
|
+
return [
|
|
588
|
+
"#compdef bt btn",
|
|
589
|
+
'# bt zsh completion \u2014 add to ~/.zshrc: eval "$(bt completion zsh)"',
|
|
590
|
+
"_bt() {",
|
|
591
|
+
" local -a cmds",
|
|
592
|
+
` cmds=(${list})`,
|
|
593
|
+
" _describe 'command' cmds",
|
|
594
|
+
"}",
|
|
595
|
+
"compdef _bt bt btn",
|
|
596
|
+
""
|
|
597
|
+
].join("\n");
|
|
598
|
+
}
|
|
599
|
+
return [
|
|
600
|
+
"# bt fish completion \u2014 bt completion fish | source",
|
|
601
|
+
"complete -c bt -f",
|
|
602
|
+
`for c in ${list}`,
|
|
603
|
+
" complete -c bt -n __fish_use_subcommand -a $c",
|
|
604
|
+
"end",
|
|
605
|
+
""
|
|
606
|
+
].join("\n");
|
|
607
|
+
}
|
|
608
|
+
function runCompletion(args) {
|
|
609
|
+
const shell = (args.shell ?? "").trim().toLowerCase();
|
|
610
|
+
if (shell !== "bash" && shell !== "zsh" && shell !== "fish") {
|
|
611
|
+
return failResult({
|
|
612
|
+
code: EXIT.DATAERR,
|
|
613
|
+
what: `unknown shell "${args.shell ?? ""}".`,
|
|
614
|
+
fix: "bt completion <bash|zsh|fish>"
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const script = completionScript(shell);
|
|
618
|
+
return { lines: [script], json: args.json ? { shell, script } : void 0 };
|
|
619
|
+
}
|
|
620
|
+
function registerCompletion(program) {
|
|
621
|
+
program.command("completion").argument("<shell>", "bash | zsh | fish").description("print a shell completion script (eval it in your shell rc).").action((shell) => {
|
|
622
|
+
const { json } = resolveOutput(program);
|
|
623
|
+
printResult(runCompletion({ shell, json }), json);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
452
627
|
// src/commands/search.ts
|
|
453
628
|
function matches(p, q) {
|
|
454
629
|
const needle = q.toLowerCase();
|
|
@@ -523,13 +698,19 @@ async function runCart(client, config, args) {
|
|
|
523
698
|
lines.push(formatCartLine(item, i, cart.currency_code, style));
|
|
524
699
|
});
|
|
525
700
|
if (cart.total != null) {
|
|
526
|
-
|
|
701
|
+
const shipNote = (cart.shipping_methods ?? []).length > 0 ? "" : dim(style, " (excl. shipping \u2014 calculated at checkout)");
|
|
702
|
+
lines.push(` total ${cyan(style, fmtPrice(cart.total, cart.currency_code))}${taxSuffix(isCartTaxInclusive(cart))}${shipNote}`);
|
|
527
703
|
}
|
|
528
704
|
lines.push(" remove one \u2192 bt unstage <#> \xB7 commit \u2192 bt commit");
|
|
529
705
|
}
|
|
530
706
|
return {
|
|
531
707
|
lines,
|
|
532
|
-
json: args.json ? {
|
|
708
|
+
json: args.json ? {
|
|
709
|
+
cart_id: cart.id,
|
|
710
|
+
items: cart.items ?? [],
|
|
711
|
+
total: cart.total ?? null,
|
|
712
|
+
total_excludes_shipping: (cart.shipping_methods ?? []).length === 0
|
|
713
|
+
} : void 0
|
|
533
714
|
};
|
|
534
715
|
} catch (e) {
|
|
535
716
|
if (e instanceof RecoverableCartError) {
|
|
@@ -641,6 +822,7 @@ async function runCheckout(client, config, args, deps) {
|
|
|
641
822
|
}
|
|
642
823
|
const itemCount = cart.items.reduce((n, li) => n + (li.quantity ?? 1), 0);
|
|
643
824
|
const totalLine = fmtPrice(shippedCart?.total ?? cart.total, cart.currency_code ?? region.currency_code);
|
|
825
|
+
const taxIncl = isCartTaxInclusive(cart);
|
|
644
826
|
const reviewLines = [header(style, "bt checkout \u2014 staged for commit")];
|
|
645
827
|
for (const li of cart.items) {
|
|
646
828
|
const colour = lineColour(li);
|
|
@@ -649,7 +831,7 @@ async function runCheckout(client, config, args, deps) {
|
|
|
649
831
|
` ${(li.title ?? li.product_title ?? "item").toString()}${colourTag} \xB7 qty ${li.quantity}` + (li.total != null ? ` ${cyan(style, fmtPrice(li.total, cart.currency_code ?? region.currency_code))}` : "")
|
|
650
832
|
);
|
|
651
833
|
}
|
|
652
|
-
reviewLines.push(` total ${cyan(style, totalLine)}
|
|
834
|
+
reviewLines.push(` total ${cyan(style, totalLine)}${taxSuffix(taxIncl)}`);
|
|
653
835
|
const print = deps.print ?? ((line) => process.stdout.write(line + "\n"));
|
|
654
836
|
for (const line of reviewLines) {
|
|
655
837
|
print(line);
|
|
@@ -657,7 +839,7 @@ async function runCheckout(client, config, args, deps) {
|
|
|
657
839
|
const autoProceed = Boolean(args.json) || Boolean(args.yes);
|
|
658
840
|
if (!autoProceed) {
|
|
659
841
|
const proceed = await prompts.confirm(
|
|
660
|
-
`proceed to checkout? ${itemCount} item${itemCount === 1 ? "" : "s"} \xB7 ${totalLine}
|
|
842
|
+
`proceed to checkout? ${itemCount} item${itemCount === 1 ? "" : "s"} \xB7 ${totalLine}${taxSuffix(taxIncl)} (y/n):`
|
|
661
843
|
);
|
|
662
844
|
if (!proceed) {
|
|
663
845
|
return {
|
|
@@ -702,7 +884,7 @@ async function runCheckout(client, config, args, deps) {
|
|
|
702
884
|
lines.push(ok("deploy successful."));
|
|
703
885
|
lines.push(` committed to wardrobe@main`);
|
|
704
886
|
lines.push(` order #${displayId}`);
|
|
705
|
-
lines.push(` total ${cyan(style, totalLine)} (incl. VAT)`);
|
|
887
|
+
lines.push(` total ${cyan(style, totalLine)}${taxIncl ? " (incl. VAT)" : ""}`);
|
|
706
888
|
if (shipsTo) {
|
|
707
889
|
lines.push(` ships to ${shipsTo} \xB7 ~3\u20136 working days`);
|
|
708
890
|
} else {
|
|
@@ -715,7 +897,15 @@ async function runCheckout(client, config, args, deps) {
|
|
|
715
897
|
return {
|
|
716
898
|
lines,
|
|
717
899
|
save,
|
|
718
|
-
json: args.json ? {
|
|
900
|
+
json: args.json ? {
|
|
901
|
+
status: "placed",
|
|
902
|
+
order_id: outcome.order_id,
|
|
903
|
+
display_id: displayId,
|
|
904
|
+
// A2 — the SHIPPING-INCLUSIVE total the buyer consented to (same figure as the review),
|
|
905
|
+
// so a --json / bt buy consumer has the real amount, not just a status.
|
|
906
|
+
total: shippedCart?.total ?? cart.total ?? null,
|
|
907
|
+
currency: cart.currency_code ?? region.currency_code ?? null
|
|
908
|
+
} : void 0
|
|
719
909
|
};
|
|
720
910
|
}
|
|
721
911
|
if (outcome.status === "expired") {
|
|
@@ -731,7 +921,7 @@ async function runCheckout(client, config, args, deps) {
|
|
|
731
921
|
fail({
|
|
732
922
|
code: 75,
|
|
733
923
|
what: "checkout handoff timed out \u2014 we didn't see the order complete.",
|
|
734
|
-
money: "
|
|
924
|
+
money: "if you completed payment, your order is safe; if not, your stage is still held \u2014 check your email or bt order.",
|
|
735
925
|
fix: "bt checkout --resume (or open the link below manually)",
|
|
736
926
|
link: checkout_url
|
|
737
927
|
})
|
|
@@ -821,40 +1011,33 @@ function timelineGraph(idx) {
|
|
|
821
1011
|
async function runOrder(client, config, args) {
|
|
822
1012
|
const email = config.email;
|
|
823
1013
|
if (!email) {
|
|
824
|
-
return {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
fix: "bt config email <you@example.com> (then retry)"
|
|
830
|
-
})
|
|
831
|
-
]
|
|
832
|
-
};
|
|
1014
|
+
return failResult({
|
|
1015
|
+
code: EXIT.DATAERR,
|
|
1016
|
+
what: "order lookup needs your email.",
|
|
1017
|
+
fix: "bt config set email <you@example.com> (then retry)"
|
|
1018
|
+
});
|
|
833
1019
|
}
|
|
834
1020
|
const looked = await client.getOrder(args.displayId, email);
|
|
835
1021
|
if (!looked) {
|
|
836
|
-
return {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
fix: "check the order number + the email it was placed with."
|
|
842
|
-
})
|
|
843
|
-
]
|
|
844
|
-
};
|
|
1022
|
+
return failResult({
|
|
1023
|
+
code: EXIT.UNAVAILABLE,
|
|
1024
|
+
what: `order ${args.displayId} not found for that email.`,
|
|
1025
|
+
fix: "check the order number + the email it was placed with."
|
|
1026
|
+
});
|
|
845
1027
|
}
|
|
846
1028
|
const o = looked.order ?? looked;
|
|
847
1029
|
const idx = stageIndex(o);
|
|
848
1030
|
const display = o.display_id ?? args.displayId;
|
|
849
1031
|
const total = fmtPrice(o.total ?? null, o.currency_code ?? null);
|
|
850
1032
|
const items = o.items ?? [];
|
|
1033
|
+
const orderTaxInclusive = items.some((i) => i.is_tax_inclusive === true);
|
|
851
1034
|
const first = items[0];
|
|
852
1035
|
const itemLabel = first ? `${first.title ?? "item"}${first.variant_title ? ` \xB7 ${first.variant_title}` : ""}` : "your order";
|
|
853
1036
|
const style = brandOf(args);
|
|
854
1037
|
const lines = [];
|
|
855
1038
|
lines.push(`${display} ${itemLabel} ${cyan(style, timelineGraph(idx))} ${STAGES[idx]}`);
|
|
856
1039
|
lines.push(` ${STAGES.join(" \u2192 ")}${idx === 4 ? " \u{1F389}" : ""}`);
|
|
857
|
-
lines.push(` total ${cyan(style, total)}
|
|
1040
|
+
lines.push(` total ${cyan(style, total)}${taxSuffix(orderTaxInclusive)}`);
|
|
858
1041
|
return {
|
|
859
1042
|
lines,
|
|
860
1043
|
json: args.json ? { display_id: display, stage: STAGES[idx], total: o.total ?? null, currency: o.currency_code ?? null } : void 0
|
|
@@ -1048,7 +1231,7 @@ async function runStatus(client, config, args) {
|
|
|
1048
1231
|
lines.push(formatCartLine(li, i, cart.currency_code, style));
|
|
1049
1232
|
});
|
|
1050
1233
|
if (cart.total != null) {
|
|
1051
|
-
lines.push(` total ${cyan(style, fmtPrice(cart.total, cart.currency_code))}
|
|
1234
|
+
lines.push(` total ${cyan(style, fmtPrice(cart.total, cart.currency_code))}${taxSuffix(isCartTaxInclusive(cart))}`);
|
|
1052
1235
|
}
|
|
1053
1236
|
lines.push(" remove one \u2192 bt unstage <#> \xB7 commit \u2192 bt commit");
|
|
1054
1237
|
}
|
|
@@ -1104,7 +1287,9 @@ async function runUnstage(client, config, args) {
|
|
|
1104
1287
|
warn(`no such item in your cart: "${args.itemId}"`),
|
|
1105
1288
|
" see what's staged (with its #) \u2192 bt cart"
|
|
1106
1289
|
],
|
|
1107
|
-
json: args.json ? { cart_id: before.id, removed: null, items: beforeItems } : void 0
|
|
1290
|
+
json: args.json ? { cart_id: before.id, removed: null, items: beforeItems } : void 0,
|
|
1291
|
+
exitCode: EXIT.DATAERR
|
|
1292
|
+
// bad ref → non-zero so a script can't proceed as if it removed something (B3)
|
|
1108
1293
|
};
|
|
1109
1294
|
}
|
|
1110
1295
|
await client.removeLineItem(config.cart_id, target.id);
|
|
@@ -1115,12 +1300,14 @@ async function runUnstage(client, config, args) {
|
|
|
1115
1300
|
return {
|
|
1116
1301
|
lines: [
|
|
1117
1302
|
fail({
|
|
1118
|
-
code:
|
|
1303
|
+
code: EXIT.GENERIC,
|
|
1119
1304
|
what: `couldn't unstage "${(target.title ?? target.product_title ?? args.itemId).toString()}" \u2014 it's still in your cart.`,
|
|
1120
1305
|
fix: "check what's staged \u2192 bt cart (then try the # shown, or bt forget to clear)"
|
|
1121
1306
|
})
|
|
1122
1307
|
],
|
|
1123
|
-
json: args.json ? { cart_id: after.id, removed: null, items: afterItems } : void 0
|
|
1308
|
+
json: args.json ? { cart_id: after.id, removed: null, items: afterItems } : void 0,
|
|
1309
|
+
exitCode: EXIT.GENERIC
|
|
1310
|
+
// the server didn't drop it → honest non-zero, never a fake success (B3)
|
|
1124
1311
|
};
|
|
1125
1312
|
}
|
|
1126
1313
|
const removedTitle = (target.title ?? target.product_title ?? "item").toString();
|
|
@@ -1145,7 +1332,8 @@ async function runUnstage(client, config, args) {
|
|
|
1145
1332
|
warn("that cart is no longer valid (expired or already committed)."),
|
|
1146
1333
|
empty(FRESH_REPO2),
|
|
1147
1334
|
" re-stage \u2192 bt browse"
|
|
1148
|
-
]
|
|
1335
|
+
],
|
|
1336
|
+
json: args.json ? { cart_id: null, removed: null, items: [] } : void 0
|
|
1149
1337
|
};
|
|
1150
1338
|
}
|
|
1151
1339
|
throw e;
|
|
@@ -1275,7 +1463,8 @@ function mergesIn(nextUntil) {
|
|
|
1275
1463
|
if (hours < 48) return `merges to main in ${hours}h`;
|
|
1276
1464
|
return `merges to main in ${Math.round(hours / 24)}d`;
|
|
1277
1465
|
}
|
|
1278
|
-
async function runDrops(client,
|
|
1466
|
+
async function runDrops(client, config, args) {
|
|
1467
|
+
const region = await client.resolveRegion(config.region);
|
|
1279
1468
|
const drops = await client.listDrops();
|
|
1280
1469
|
if (!drops || drops.length === 0) {
|
|
1281
1470
|
return {
|
|
@@ -1291,7 +1480,7 @@ async function runDrops(client, _config, args) {
|
|
|
1291
1480
|
lines.push(
|
|
1292
1481
|
[
|
|
1293
1482
|
` ${(d.handle ?? d.id).padEnd(32)}`,
|
|
1294
|
-
fmtPrice(d.price,
|
|
1483
|
+
fmtPrice(d.price, region.currency_code).padEnd(10),
|
|
1295
1484
|
mergesIn(d.next_until)
|
|
1296
1485
|
].join(" ").trimEnd()
|
|
1297
1486
|
);
|
|
@@ -1344,6 +1533,35 @@ function registerConnect(program) {
|
|
|
1344
1533
|
// src/commands/login.ts
|
|
1345
1534
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1346
1535
|
async function runLogin(client, prompts, keychain, config, args) {
|
|
1536
|
+
if (args.json) {
|
|
1537
|
+
const jsonEmail = (args.email ?? "").trim();
|
|
1538
|
+
const jsonToken = (args.token ?? "").trim();
|
|
1539
|
+
if (!jsonEmail || !EMAIL_RE.test(jsonEmail) || !jsonToken) {
|
|
1540
|
+
return failResult({
|
|
1541
|
+
code: EXIT.DATAERR,
|
|
1542
|
+
what: "login --json is non-interactive \u2014 it needs --email and a token in BT_MAGIC_TOKEN.",
|
|
1543
|
+
fix: "bt login --email you@dev.to with BT_MAGIC_TOKEN=<the token from the email>"
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
let jsonResult;
|
|
1547
|
+
try {
|
|
1548
|
+
jsonResult = await client.verifyMagicLink(jsonToken);
|
|
1549
|
+
} catch (e) {
|
|
1550
|
+
if (e instanceof StoreApiError && (e.status === 401 || e.status === 403)) {
|
|
1551
|
+
return failResult({ code: EXIT.NOPERM, what: "that token is invalid or expired.", fix: "request a fresh one, then set BT_MAGIC_TOKEN" });
|
|
1552
|
+
}
|
|
1553
|
+
if (e instanceof StoreApiError) {
|
|
1554
|
+
return failResult({ code: EXIT.TEMPFAIL, what: "login couldn't complete right now.", fix: "try again in a moment" });
|
|
1555
|
+
}
|
|
1556
|
+
throw e;
|
|
1557
|
+
}
|
|
1558
|
+
keychain.setToken(jsonResult.jwt);
|
|
1559
|
+
return {
|
|
1560
|
+
lines: [ok("authenticated. welcome \u2014 you're a contributor now.")],
|
|
1561
|
+
save: { email: jsonEmail },
|
|
1562
|
+
json: { connected: true, customer_id: jsonResult.customer.id }
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1347
1565
|
let email = (args.email ?? "").trim();
|
|
1348
1566
|
if (!email) {
|
|
1349
1567
|
email = (await prompts.ask("email", { default: config.email })).trim();
|
|
@@ -1410,11 +1628,7 @@ function registerLogin(program, makeClient2, loadConfig2, prompts, keychain, per
|
|
|
1410
1628
|
if (res.save) {
|
|
1411
1629
|
persist(res.save);
|
|
1412
1630
|
}
|
|
1413
|
-
|
|
1414
|
-
process.stdout.write(JSON.stringify(res.json) + "\n");
|
|
1415
|
-
} else {
|
|
1416
|
-
process.stdout.write(res.lines.join("\n") + "\n");
|
|
1417
|
-
}
|
|
1631
|
+
printResult(res, json);
|
|
1418
1632
|
});
|
|
1419
1633
|
}
|
|
1420
1634
|
|
|
@@ -1425,13 +1639,15 @@ async function runLogout(keychain) {
|
|
|
1425
1639
|
lines: [
|
|
1426
1640
|
ok("logged out. your token is wiped from this machine's keychain."),
|
|
1427
1641
|
empty(" (server tokens are short-lived by design \u2014 they expire on their own.)")
|
|
1428
|
-
]
|
|
1642
|
+
],
|
|
1643
|
+
json: { logged_out: true }
|
|
1429
1644
|
};
|
|
1430
1645
|
}
|
|
1431
1646
|
function registerLogout(program, keychain) {
|
|
1432
1647
|
program.command("logout").description("clear your local session (wipes the keychain token).").action(async () => {
|
|
1648
|
+
const { json } = resolveOutput(program);
|
|
1433
1649
|
const res = await runLogout(keychain);
|
|
1434
|
-
|
|
1650
|
+
printResult(res, json);
|
|
1435
1651
|
});
|
|
1436
1652
|
}
|
|
1437
1653
|
|
|
@@ -1472,7 +1688,9 @@ async function runDrop(args, exec) {
|
|
|
1472
1688
|
if (code !== 0) {
|
|
1473
1689
|
return {
|
|
1474
1690
|
lines: [warn(`drop ${sub} reported a problem (exit ${code}). check the backend logs / ops alert.`)],
|
|
1475
|
-
json: args.json ? { sub, drop_id: args.dropId, exit: code } : void 0
|
|
1691
|
+
json: args.json ? { sub, drop_id: args.dropId, exit: code } : void 0,
|
|
1692
|
+
exitCode: code
|
|
1693
|
+
// forward the backend's exit code so operator CI can branch on $? (B3)
|
|
1476
1694
|
};
|
|
1477
1695
|
}
|
|
1478
1696
|
const done = {
|
|
@@ -1490,16 +1708,12 @@ function registerDrop(program, exec) {
|
|
|
1490
1708
|
program.command("drop", { hidden: true }).argument("<sub>", "create | verify | promote | retire").option("--motif <slug>", "motif slug (create)").option("--drop-id <id>", "the drop id").option("--stage <stage>", "expected stage for verify (next|main)").description("operator: manage a drop's lifecycle (create/verify/promote/retire).").action(async (sub, opts) => {
|
|
1491
1709
|
const { json } = resolveOutput(program);
|
|
1492
1710
|
const operator = process.env.BT_OPERATOR === "1";
|
|
1493
|
-
process.stdout.write(working(`drop ${sub}\u2026`) + "\n");
|
|
1711
|
+
if (!json) process.stdout.write(working(`drop ${sub}\u2026`) + "\n");
|
|
1494
1712
|
const res = await runDrop(
|
|
1495
1713
|
{ sub, motif: opts.motif, dropId: opts.dropId, stage: opts.stage, operator, json },
|
|
1496
1714
|
exec
|
|
1497
1715
|
);
|
|
1498
|
-
|
|
1499
|
-
process.stdout.write(JSON.stringify(res.json) + "\n");
|
|
1500
|
-
} else {
|
|
1501
|
-
process.stdout.write(res.lines.join("\n") + "\n");
|
|
1502
|
-
}
|
|
1716
|
+
printResult(res, json);
|
|
1503
1717
|
});
|
|
1504
1718
|
}
|
|
1505
1719
|
|
|
@@ -1611,21 +1825,19 @@ function runConfig(args, deps) {
|
|
|
1611
1825
|
}
|
|
1612
1826
|
if (sub === "set") {
|
|
1613
1827
|
if (!args.key || args.value == null || args.value === "") {
|
|
1614
|
-
return {
|
|
1828
|
+
return failResult({ code: EXIT.DATAERR, what: "config set needs a key and a value.", fix: "bt config set <key> <value>" });
|
|
1615
1829
|
}
|
|
1616
1830
|
if (!SETTABLE.includes(args.key)) {
|
|
1617
|
-
return {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
]
|
|
1623
|
-
};
|
|
1831
|
+
return failResult({
|
|
1832
|
+
code: EXIT.DATAERR,
|
|
1833
|
+
what: `can't set "${args.key}".`,
|
|
1834
|
+
fix: `settable keys: ${SETTABLE.join(", ")} (the auth token is never stored in config \u2014 use bt login)`
|
|
1835
|
+
});
|
|
1624
1836
|
}
|
|
1625
1837
|
deps.save({ [args.key]: args.value });
|
|
1626
1838
|
return { lines: [ok(`set ${args.key} = ${args.value}`)], json: args.json ? { [args.key]: args.value } : void 0 };
|
|
1627
1839
|
}
|
|
1628
|
-
return {
|
|
1840
|
+
return failResult({ code: EXIT.DATAERR, what: `unknown config command "${sub}".`, fix: "bt config get [key] | bt config set <key> <value>" });
|
|
1629
1841
|
}
|
|
1630
1842
|
function registerConfig(program) {
|
|
1631
1843
|
program.command("config").argument("[sub]", "get | set", "get").argument("[key]", "config key").argument("[value]", "value (for set)").description("view or set local prefs (email, region, defaults). the auth token is never stored here.").action(async (sub, key, value) => {
|
|
@@ -1644,7 +1856,9 @@ function runDoctor(s, json) {
|
|
|
1644
1856
|
ok("bt doctor \u2014 local checkup (nothing was sent)"),
|
|
1645
1857
|
` bt ${s.btVersion}`,
|
|
1646
1858
|
` node ${s.node} \xB7 ${s.platform}`,
|
|
1647
|
-
|
|
1859
|
+
// E4 — a friendly label for the production store; the raw URL only when pointed elsewhere
|
|
1860
|
+
// (local/staging), where it's genuinely diagnostic. The JSON snapshot keeps the raw URL.
|
|
1861
|
+
` store ${s.storeUrl === BT_DEFAULT_STORE_URL ? "binaryandthread.com (production)" : s.storeUrl}`,
|
|
1648
1862
|
` key set (${s.keySource})`,
|
|
1649
1863
|
` auth ${auth}`,
|
|
1650
1864
|
` terminal ${s.tty ? "tty" : "non-tty"} \xB7 colour ${s.color ? "on" : "off"}`,
|
|
@@ -1793,7 +2007,7 @@ function buildProgram() {
|
|
|
1793
2007
|
const program = new Command();
|
|
1794
2008
|
program.name("bt").description(
|
|
1795
2009
|
"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)"
|
|
1796
|
-
).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();
|
|
2010
|
+
).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)").option("--region <cc>", "ship-to country for this command (ISO-2, overrides your saved region)").showSuggestionAfterError();
|
|
1797
2011
|
program.addHelpText("beforeAll", "\n bt \u2014 the binary & thread terminal store. new here? \u2192 bt init\n");
|
|
1798
2012
|
program.command("brew").argument("[what]", "what to brew").description("we make clothing, not coffee.").action(() => {
|
|
1799
2013
|
process.stdout.write(teapot() + "\n");
|
|
@@ -1802,7 +2016,9 @@ function buildProgram() {
|
|
|
1802
2016
|
program.command("forget").description("wipe your local session + config (keychain token, cart, email, defaults).").action(() => {
|
|
1803
2017
|
deleteToken();
|
|
1804
2018
|
forgetConfig();
|
|
1805
|
-
|
|
2019
|
+
const json = Boolean(program.opts().json);
|
|
2020
|
+
if (json) process.stdout.write(JSON.stringify({ wiped: true }) + "\n");
|
|
2021
|
+
else process.stdout.write("\u2713 local session + config wiped. (your wardrobe on the server is untouched.)\n");
|
|
1806
2022
|
});
|
|
1807
2023
|
registerCommands(program);
|
|
1808
2024
|
return program;
|
|
@@ -1822,34 +2038,41 @@ var dropExec = {
|
|
|
1822
2038
|
}
|
|
1823
2039
|
};
|
|
1824
2040
|
function registerCommands(program) {
|
|
2041
|
+
const persist = (patch) => {
|
|
2042
|
+
saveConfig(patch);
|
|
2043
|
+
};
|
|
2044
|
+
const effectiveConfig = () => {
|
|
2045
|
+
const saved = loadConfig();
|
|
2046
|
+
const flag = program.opts().region;
|
|
2047
|
+
return flag ? { ...saved, region: flag.trim().toLowerCase() } : saved;
|
|
2048
|
+
};
|
|
1825
2049
|
registerInit(program);
|
|
1826
|
-
registerBrowse(program, makeClient,
|
|
1827
|
-
registerSearch(program, makeClient,
|
|
1828
|
-
registerView(program, makeClient,
|
|
1829
|
-
registerStage(program, makeClient,
|
|
1830
|
-
registerBuy(program, makeClient,
|
|
2050
|
+
registerBrowse(program, makeClient, effectiveConfig);
|
|
2051
|
+
registerSearch(program, makeClient, effectiveConfig);
|
|
2052
|
+
registerView(program, makeClient, effectiveConfig);
|
|
2053
|
+
registerStage(program, makeClient, effectiveConfig);
|
|
2054
|
+
registerBuy(program, makeClient, effectiveConfig);
|
|
1831
2055
|
registerConfig(program);
|
|
2056
|
+
registerRegion(program, makeClient, effectiveConfig, persist);
|
|
2057
|
+
registerCompletion(program);
|
|
1832
2058
|
registerToken(program);
|
|
1833
2059
|
registerDoctor(program);
|
|
1834
2060
|
registerReport(program, makeClient);
|
|
1835
|
-
registerBlame(program, makeClient,
|
|
1836
|
-
registerCart(program, makeClient,
|
|
1837
|
-
registerCheckout(program, makeClient,
|
|
1838
|
-
registerOrder(program, makeClient,
|
|
1839
|
-
registerLog(program, makeClient,
|
|
1840
|
-
registerStatus(program, makeClient,
|
|
1841
|
-
registerWhoami(program, makeClient,
|
|
1842
|
-
registerDrops(program, makeClient,
|
|
2061
|
+
registerBlame(program, makeClient, effectiveConfig);
|
|
2062
|
+
registerCart(program, makeClient, effectiveConfig);
|
|
2063
|
+
registerCheckout(program, makeClient, effectiveConfig);
|
|
2064
|
+
registerOrder(program, makeClient, effectiveConfig);
|
|
2065
|
+
registerLog(program, makeClient, effectiveConfig);
|
|
2066
|
+
registerStatus(program, makeClient, effectiveConfig);
|
|
2067
|
+
registerWhoami(program, makeClient, effectiveConfig);
|
|
2068
|
+
registerDrops(program, makeClient, effectiveConfig);
|
|
1843
2069
|
registerConnect(program);
|
|
1844
|
-
registerUnstage(program, makeClient,
|
|
2070
|
+
registerUnstage(program, makeClient, effectiveConfig);
|
|
1845
2071
|
const prompts = makePrompts();
|
|
1846
|
-
const persist = (patch) => {
|
|
1847
|
-
saveConfig(patch);
|
|
1848
|
-
};
|
|
1849
2072
|
registerLogin(
|
|
1850
2073
|
program,
|
|
1851
2074
|
makeClient,
|
|
1852
|
-
|
|
2075
|
+
effectiveConfig,
|
|
1853
2076
|
prompts,
|
|
1854
2077
|
{ setToken: (jwt) => setToken(jwt) },
|
|
1855
2078
|
persist
|