@binaryandthread/cli 0.2.1 → 0.3.1

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
@@ -5,7 +5,7 @@ import {
5
5
  forgetConfig,
6
6
  loadConfig,
7
7
  saveConfig
8
- } from "./chunk-OVG37IIX.js";
8
+ } from "./chunk-ZNDKXHMV.js";
9
9
  import {
10
10
  makePrompts
11
11
  } from "./chunk-AAN4VWP3.js";
@@ -53,7 +53,7 @@ import {
53
53
  versionLine,
54
54
  warn,
55
55
  working
56
- } from "./chunk-AP5ARNWU.js";
56
+ } from "./chunk-VDOFLQ2P.js";
57
57
  import {
58
58
  __require
59
59
  } from "./chunk-7OCVIDC7.js";
@@ -62,6 +62,26 @@ import {
62
62
  import { spawn } from "child_process";
63
63
  import { Command } from "commander";
64
64
 
65
+ // src/locale.ts
66
+ function detectRegionFromLocale(env) {
67
+ const raw = (env.LC_ALL || env.LC_MESSAGES || env.LANG || "").trim();
68
+ const m = /^[A-Za-z]{2,3}[_-]([A-Za-z]{2})(?:[.@]|$)/.exec(raw);
69
+ return m ? m[1].toLowerCase() : null;
70
+ }
71
+ function resolveEffectiveRegion(flag, saved, env) {
72
+ if (flag && flag.trim()) {
73
+ return { region: flag.trim().toLowerCase(), source: "flag" };
74
+ }
75
+ if (saved && saved.trim()) {
76
+ return { region: saved.trim().toLowerCase(), source: "config" };
77
+ }
78
+ const fromLocale = detectRegionFromLocale(env);
79
+ if (fromLocale) {
80
+ return { region: fromLocale, source: "locale" };
81
+ }
82
+ return { region: void 0, source: void 0 };
83
+ }
84
+
65
85
  // src/client-config.ts
66
86
  function resolveClientConfig(env = process.env) {
67
87
  const baseUrl = env.BT_API_URL || env.MEDUSA_BACKEND_URL || BT_DEFAULT_STORE_URL;
@@ -82,7 +102,7 @@ function makeLogger(debug, write = (s) => process.stderr.write(s)) {
82
102
  }
83
103
 
84
104
  // src/version.ts
85
- var VERSION = "0.2.1";
105
+ var VERSION = "0.3.1";
86
106
 
87
107
  // src/eggs.ts
88
108
  function teapot() {
@@ -222,7 +242,8 @@ function isMainModule(argv1, moduleUrl, realpath = realpathSync) {
222
242
  import * as fs from "fs";
223
243
  import * as path from "path";
224
244
  var CACHE_FILE = "update-check.json";
225
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
245
+ var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
246
+ var NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
226
247
  var REGISTRY_LATEST = "https://registry.npmjs.org/@binaryandthread/cli/latest";
227
248
  var FETCH_TIMEOUT_MS = 2e3;
228
249
  function parseSemver(v) {
@@ -254,9 +275,14 @@ function maybeNotifyUpdate(deps) {
254
275
  const now = deps.now ?? Date.now();
255
276
  const read = deps.readCache ?? readUpdateCache;
256
277
  const refresh = deps.refresh ?? ((n) => void fetchLatestIntoCache(n));
278
+ const markNotified = deps.markNotified ?? markNotifiedInCache;
257
279
  const cache = read();
258
280
  const line = updateNoticeLine(deps.current, cache?.latest);
259
- if (line) deps.write(line + "\n");
281
+ const canNotify = cache?.lastNotified === void 0 || now - cache.lastNotified >= NOTICE_INTERVAL_MS;
282
+ if (line && canNotify) {
283
+ deps.write(line + "\n");
284
+ markNotified(now);
285
+ }
260
286
  if (!cache || now - cache.lastCheck > CHECK_INTERVAL_MS) {
261
287
  refresh(now);
262
288
  }
@@ -268,12 +294,22 @@ function readUpdateCache() {
268
294
  try {
269
295
  const j = JSON.parse(fs.readFileSync(cachePath(), "utf8"));
270
296
  if (j && typeof j.latest === "string" && typeof j.lastCheck === "number") {
271
- return { latest: j.latest, lastCheck: j.lastCheck };
297
+ const cache = { latest: j.latest, lastCheck: j.lastCheck };
298
+ if (typeof j.lastNotified === "number") {
299
+ cache.lastNotified = j.lastNotified;
300
+ }
301
+ return cache;
272
302
  }
273
303
  } catch {
274
304
  }
275
305
  return null;
276
306
  }
307
+ function markNotifiedInCache(now) {
308
+ const cache = readUpdateCache();
309
+ if (cache) {
310
+ writeUpdateCache({ ...cache, lastNotified: now });
311
+ }
312
+ }
277
313
  function writeUpdateCache(c) {
278
314
  try {
279
315
  fs.mkdirSync(configDir(), { recursive: true });
@@ -439,7 +475,7 @@ async function runBrowse(client, config, args) {
439
475
  lines.push(" peek at next \u2192 bt browse --branch=next");
440
476
  } else {
441
477
  lines.push(header(style, "bt browse \u2014 in stock"));
442
- lines.push(regionLine(rr, style));
478
+ lines.push(regionLine(rr, style, config.region_source));
443
479
  lines.push(...layoutRows(rows, style));
444
480
  for (const d of drops) {
445
481
  const dt = (d.title ?? d.handle ?? d.id).toString();
@@ -478,7 +514,7 @@ function registerBrowse(program, makeClient2, loadConfig2) {
478
514
  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) => {
479
515
  const { json, brand } = resolveOutput(program);
480
516
  if (opts.pick && !json && brand.color && process.stdout.isTTY && process.stdin.isTTY) {
481
- const { runInteractiveBrowse } = await import("./interactive-4XWSUOOK.js");
517
+ const { runInteractiveBrowse } = await import("./interactive-JALKLT56.js");
482
518
  const handled = await runInteractiveBrowse(makeClient2(), loadConfig2(), { branch: opts.branch, brand });
483
519
  if (handled) return;
484
520
  }
@@ -820,8 +856,13 @@ async function runCheckout(client, config, args, deps) {
820
856
  // fail-closed, no order placed → non-zero (matches the shown exit 1)
821
857
  };
822
858
  }
859
+ const email = (config.email ?? "").trim();
860
+ const mint = await client.mintCheckout(cartId, email || void 0, "cli");
861
+ const { checkout_url, session_id } = mint;
862
+ const save = email ? { email } : {};
863
+ const currency = mint.currency ?? cart.currency_code ?? region.currency_code;
823
864
  const itemCount = cart.items.reduce((n, li) => n + (li.quantity ?? 1), 0);
824
- const totalLine = fmtPrice(shippedCart?.total ?? cart.total, cart.currency_code ?? region.currency_code);
865
+ const totalLine = fmtPrice(mint.total ?? shippedCart?.total ?? cart.total, currency);
825
866
  const taxIncl = isCartTaxInclusive(cart);
826
867
  const reviewLines = [header(style, "bt checkout \u2014 staged for commit")];
827
868
  for (const li of cart.items) {
@@ -831,6 +872,11 @@ async function runCheckout(client, config, args, deps) {
831
872
  ` ${(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))}` : "")
832
873
  );
833
874
  }
875
+ if (mint.discount && mint.discount.amount > 0) {
876
+ reviewLines.push(
877
+ dim(style, ` dev channel \u2212${fmtPrice(mint.discount.amount, currency)} (${mint.discount.pct}% \u2014 for shopping the terminal)`)
878
+ );
879
+ }
834
880
  reviewLines.push(` total ${cyan(style, totalLine)}${taxSuffix(taxIncl)}`);
835
881
  const print = deps.print ?? ((line) => process.stdout.write(line + "\n"));
836
882
  for (const line of reviewLines) {
@@ -849,9 +895,6 @@ async function runCheckout(client, config, args, deps) {
849
895
  }
850
896
  const allowed = supportedCountries(region);
851
897
  const shipsTo = (allowed[0] ?? "").toUpperCase();
852
- const email = (config.email ?? "").trim();
853
- const { checkout_url, session_id } = await client.mintCheckout(cartId, email || void 0, "cli");
854
- const save = email ? { email } : {};
855
898
  const opened = await deps.open(checkout_url);
856
899
  for (const line of opened.lines) {
857
900
  print(line);
@@ -884,6 +927,9 @@ async function runCheckout(client, config, args, deps) {
884
927
  lines.push(ok("deploy successful."));
885
928
  lines.push(` committed to wardrobe@main`);
886
929
  lines.push(` order #${displayId}`);
930
+ if (mint.discount && mint.discount.amount > 0) {
931
+ lines.push(dim(style, ` dev channel \u2212${fmtPrice(mint.discount.amount, currency)} (${mint.discount.pct}% \u2014 the terminal's cut for you)`));
932
+ }
887
933
  lines.push(` total ${cyan(style, totalLine)}${taxIncl ? " (incl. VAT)" : ""}`);
888
934
  if (shipsTo) {
889
935
  lines.push(` ships to ${shipsTo} \xB7 ~3\u20136 working days`);
@@ -901,10 +947,11 @@ async function runCheckout(client, config, args, deps) {
901
947
  status: "placed",
902
948
  order_id: outcome.order_id,
903
949
  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
950
+ // A2 — the SHIPPING-INCLUSIVE, dev-channel-discounted total the buyer consented to (same
951
+ // figure as the review), so a --json / bt buy consumer has the real amount, not just a status.
952
+ total: mint.total ?? shippedCart?.total ?? cart.total ?? null,
953
+ currency: mint.currency ?? cart.currency_code ?? region.currency_code ?? null,
954
+ discount: mint.discount ?? null
908
955
  } : void 0
909
956
  };
910
957
  }
@@ -962,7 +1009,7 @@ function registerCheckout(program, makeClient2, loadConfig2) {
962
1009
  const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
963
1010
  const { openCheckout } = await import("./browser-WCNC4AWS.js");
964
1011
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
965
- const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
1012
+ const { saveConfig: saveConfig2 } = await import("./config-GVEB4R7I.js");
966
1013
  const client = makeClient2();
967
1014
  const print = json ? () => {
968
1015
  } : (line) => process.stdout.write(line + "\n");
@@ -1348,52 +1395,31 @@ function registerUnstage(program, makeClient2, loadConfig2) {
1348
1395
  }
1349
1396
 
1350
1397
  // src/commands/whoami.ts
1351
- var NEXT_TIER = {
1352
- contributor: { at: 5, name: "maintainer", pct: 10 },
1353
- maintainer: { at: 20, name: "core", pct: 15 },
1354
- core: { at: 0, name: "core", pct: 20 }
1355
- };
1398
+ var DEV_CHANNEL_PCT = 10;
1356
1399
  var GRAPH_CAP = 20;
1357
1400
  var CARD_WIDTH = 58;
1358
1401
  function profileCard(p, style, handle) {
1359
- const tier = p.bt_tier ?? "contributor";
1360
1402
  const commits = p.bt_commit_count ?? 0;
1361
- const next = NEXT_TIER[tier];
1362
- const isMax = tier === "core" || !next;
1363
- const toNext = next && !isMax ? Math.max(0, next.at - commits) : 0;
1364
- const standing = p.standing_pct ?? NEXT_TIER[tier]?.pct ?? null;
1365
1403
  const inner = [];
1366
- inner.push(`${handle ? `${handle} \xB7 ` : ""}${cyan(style, tier)}`);
1404
+ inner.push(`${handle ? `${handle} \xB7 ` : ""}${cyan(style, "terminal")}`);
1367
1405
  inner.push(rule(CARD_WIDTH - 1));
1368
- if (isMax) {
1369
- inner.push(`tier ${tier.padEnd(13)}max standing \u2014 ${standing ?? 20}%`);
1370
- } else {
1371
- inner.push(`tier ${tier.padEnd(13)}${toNext} more commit${toNext === 1 ? "" : "s"} to ${next.name}`);
1372
- }
1373
1406
  if (commits === 0) {
1374
- inner.push(`commits ${String(commits).padEnd(13)}your repo's empty \u2014 everyone starts at 0`);
1407
+ inner.push(`orders ${String(commits).padEnd(13)}your repo's empty \u2014 everyone starts at 0`);
1375
1408
  } else {
1376
- inner.push(`commits ${String(commits).padEnd(13)}order${commits === 1 ? "" : "s"} committed`);
1377
- }
1378
- if (standing != null) {
1379
- inner.push(`standing ${cyan(style, `${standing}%`.padEnd(13))}applied at every commit`);
1409
+ inner.push(`orders ${String(commits).padEnd(13)}committed`);
1380
1410
  }
1411
+ inner.push(`discount ${cyan(style, `${DEV_CHANNEL_PCT}%`.padEnd(13))}off every order \u2014 for shopping the terminal`);
1381
1412
  const bar = contributionBar(style, commits, GRAPH_CAP);
1382
1413
  const graphLabel = commits === 0 ? "no commits yet" : `${commits} contribution${commits === 1 ? "" : "s"}`;
1383
1414
  inner.push(`graph ${bar} ${graphLabel}`);
1384
- if (p.bt_badges && p.bt_badges.length > 0) {
1385
- inner.push(`badges ${p.bt_badges.join(" \xB7 ")}`);
1386
- }
1387
1415
  if (p.bt_connected_at) {
1388
- inner.push(`since contributor since ${p.bt_connected_at.slice(0, 10)}`);
1416
+ inner.push(`since connected ${p.bt_connected_at.slice(0, 10)}`);
1389
1417
  }
1390
1418
  inner.push(rule(CARD_WIDTH - 1));
1391
- if (isMax) {
1392
- inner.push(`in ${green(style, "AUTHORS")}. earned, not bought.`);
1393
- } else if (commits === 0) {
1419
+ if (commits === 0) {
1394
1420
  inner.push("your first commit makes this worth screenshotting \u2192 bt browse");
1395
1421
  } else {
1396
- inner.push(`${toNext} more to ${next.name} \u2192 bt browse \xB7 earned, not bought`);
1422
+ inner.push(`in ${green(style, "AUTHORS")}. earned, not bought.`);
1397
1423
  }
1398
1424
  return box(inner, { width: CARD_WIDTH, title: "bt whoami", style });
1399
1425
  }
@@ -1414,10 +1440,8 @@ async function runWhoami(client, config, args, token) {
1414
1440
  lines: profileCard(me, style, handle),
1415
1441
  json: {
1416
1442
  authenticated: true,
1417
- tier: me.bt_tier,
1418
- commits: me.bt_commit_count,
1419
- badges: me.bt_badges,
1420
- standing_pct: me.standing_pct ?? null,
1443
+ orders: me.bt_commit_count ?? 0,
1444
+ discount_pct: DEV_CHANNEL_PCT,
1421
1445
  connected_at: me.bt_connected_at,
1422
1446
  token_expires_at: expiresAt,
1423
1447
  token_expired: expired
@@ -1430,11 +1454,10 @@ async function runWhoami(client, config, args, token) {
1430
1454
  return {
1431
1455
  lines: [
1432
1456
  empty("you're browsing as a guest \u2014 not connected yet."),
1433
- " connect \u2192 bt login (the contributor on-ramp for this channel)"
1457
+ " connect \u2192 bt login (order history + a persistent cart; the terminal's 10% applies either way)"
1434
1458
  ],
1435
1459
  json: args.json ? {
1436
1460
  authenticated: false,
1437
- tier: null,
1438
1461
  connected: false,
1439
1462
  token_expires_at: expiresAt,
1440
1463
  token_expired: expired
@@ -1445,7 +1468,7 @@ async function runWhoami(client, config, args, token) {
1445
1468
  }
1446
1469
  }
1447
1470
  function registerWhoami(program, makeClient2, loadConfig2) {
1448
- program.command("whoami").description("your contribution profile + standing.").action(async () => {
1471
+ program.command("whoami").description("your profile + order history.").action(async () => {
1449
1472
  const { json, brand } = resolveOutput(program);
1450
1473
  const res = await runWhoami(makeClient2(), loadConfig2(), { json, brand }, getToken());
1451
1474
  printResult(res, json);
@@ -1505,31 +1528,6 @@ function registerDrops(program, makeClient2, loadConfig2) {
1505
1528
  });
1506
1529
  }
1507
1530
 
1508
- // src/commands/connect.ts
1509
- async function runConnect(args) {
1510
- const lines = [
1511
- "connect your wardrobe \u2014 become a contributor.",
1512
- " connecting authenticates you and unlocks:",
1513
- " \xB7 10% standing (applied at every commit)",
1514
- " \xB7 access to next (drops before the web sees them)",
1515
- " \xB7 your wardrobe repo + the contributor badge",
1516
- "",
1517
- " connect now \u2192 bt login",
1518
- " (we email you a token to paste; it lives in your OS keychain, never in plaintext config.)"
1519
- ];
1520
- return {
1521
- lines,
1522
- json: args.json ? { connected: false, login_command: "bt login" } : void 0
1523
- };
1524
- }
1525
- function registerConnect(program) {
1526
- program.command("connect").description("the contributor on-ramp \u2014 what connecting unlocks. run bt login to connect.").action(async () => {
1527
- const { json } = resolveOutput(program);
1528
- const res = await runConnect({ json });
1529
- printResult(res, json);
1530
- });
1531
- }
1532
-
1533
1531
  // src/commands/login.ts
1534
1532
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1535
1533
  async function runLogin(client, prompts, keychain, config, args) {
@@ -1557,7 +1555,7 @@ async function runLogin(client, prompts, keychain, config, args) {
1557
1555
  }
1558
1556
  keychain.setToken(jsonResult.jwt);
1559
1557
  return {
1560
- lines: [ok("authenticated. welcome \u2014 you're a contributor now.")],
1558
+ lines: [ok("authenticated. welcome back.")],
1561
1559
  save: { email: jsonEmail },
1562
1560
  json: { connected: true, customer_id: jsonResult.customer.id }
1563
1561
  };
@@ -1608,9 +1606,9 @@ async function runLogin(client, prompts, keychain, config, args) {
1608
1606
  keychain.setToken(result.jwt);
1609
1607
  return {
1610
1608
  lines: [
1611
- ok("authenticated. welcome \u2014 you're a contributor now."),
1609
+ ok("authenticated. welcome back."),
1612
1610
  ` ${GLYPH.arrow} your token is in your OS keychain (never plaintext).`,
1613
- ` ${GLYPH.arrow} see your standing \u2192 bt whoami`
1611
+ ` ${GLYPH.arrow} your profile \u2192 bt whoami \xB7 order history \u2192 bt log`
1614
1612
  ],
1615
1613
  save: { email },
1616
1614
  json: args.json ? { connected: true, customer_id: result.customer.id } : void 0
@@ -1725,20 +1723,24 @@ function runInit(args) {
1725
1723
  const lines = [
1726
1724
  header(style, "bt \u2014 binary & thread, the terminal store"),
1727
1725
  " buy heavyweight pieces from your terminal. zero setup \u2014 you're already pointed at the live store.",
1726
+ " shopping here is 10% off the web \u2014 always, applied at checkout. no code, no login.",
1728
1727
  "",
1729
1728
  " the happy path:",
1730
1729
  " bt browse # the catalog, numbered",
1731
1730
  " bt stage <n|slug> --size L # add to your cart (a browse number, or a slug)",
1732
- " bt checkout # pay in your browser \u2014 cards never touch the terminal",
1731
+ " bt checkout # pay in your browser \u2014 cards never touch the terminal (10% applied)",
1732
+ "",
1733
+ " ships worldwide \u2014 set your country so prices show your currency:",
1734
+ " bt region <cc> # e.g. se / us / de \xB7 see all \u2192 bt region --list",
1733
1735
  "",
1734
1736
  " optional:",
1735
- " bt login # contributor standing + order history",
1737
+ " bt login # order history + a persistent cart",
1736
1738
  " bt whoami # your profile",
1737
1739
  ` docs \u2192 ${DOCS_URL}`
1738
1740
  ];
1739
1741
  return {
1740
1742
  lines,
1741
- json: args.json ? { happy_path: HAPPY_PATH, docs: DOCS_URL, zero_setup: true } : void 0
1743
+ json: args.json ? { happy_path: HAPPY_PATH, docs: DOCS_URL, zero_setup: true, dev_channel_discount_pct: 10 } : void 0
1742
1744
  };
1743
1745
  }
1744
1746
  function registerInit(program) {
@@ -1782,7 +1784,7 @@ function registerBuy(program, makeClient2, loadConfig2) {
1782
1784
  const { makePrompts: makePrompts2 } = await import("./prompts-DDVV5RE6.js");
1783
1785
  const { openCheckout } = await import("./browser-WCNC4AWS.js");
1784
1786
  const { pollUntilPlaced } = await import("./poll-O3VUD5WV.js");
1785
- const { saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
1787
+ const { saveConfig: saveConfig2 } = await import("./config-GVEB4R7I.js");
1786
1788
  const client = makeClient2();
1787
1789
  const print = json ? () => {
1788
1790
  } : (line) => process.stdout.write(line + "\n");
@@ -1842,7 +1844,7 @@ function runConfig(args, deps) {
1842
1844
  function registerConfig(program) {
1843
1845
  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) => {
1844
1846
  const { json } = resolveOutput(program);
1845
- const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await import("./config-FXGCWC6Y.js");
1847
+ const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await import("./config-GVEB4R7I.js");
1846
1848
  const res = runConfig({ sub, key, value, json }, { load: loadConfig2, save: saveConfig2 });
1847
1849
  printResult(res, json);
1848
1850
  });
@@ -2044,7 +2046,8 @@ function registerCommands(program) {
2044
2046
  const effectiveConfig = () => {
2045
2047
  const saved = loadConfig();
2046
2048
  const flag = program.opts().region;
2047
- return flag ? { ...saved, region: flag.trim().toLowerCase() } : saved;
2049
+ const { region, source } = resolveEffectiveRegion(flag, saved.region, process.env);
2050
+ return { ...saved, region, region_source: source };
2048
2051
  };
2049
2052
  registerInit(program);
2050
2053
  registerBrowse(program, makeClient, effectiveConfig);
@@ -2066,7 +2069,6 @@ function registerCommands(program) {
2066
2069
  registerStatus(program, makeClient, effectiveConfig);
2067
2070
  registerWhoami(program, makeClient, effectiveConfig);
2068
2071
  registerDrops(program, makeClient, effectiveConfig);
2069
- registerConnect(program);
2070
2072
  registerUnstage(program, makeClient, effectiveConfig);
2071
2073
  const prompts = makePrompts();
2072
2074
  registerLogin(