terminalmarket 0.13.2 → 0.14.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.
Files changed (3) hide show
  1. package/bin/tm.js +222 -125
  2. package/package.json +1 -1
  3. package/src/format.js +20 -0
package/bin/tm.js CHANGED
@@ -26,6 +26,18 @@ const __filename = fileURLToPath(import.meta.url);
26
26
  const __dirname = dirname(__filename);
27
27
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
28
28
  const VERSION = pkg.version;
29
+ const commandManifest = (() => {
30
+ try {
31
+ return JSON.parse(
32
+ readFileSync(
33
+ join(__dirname, "../../../shared/command-manifest.json"),
34
+ "utf-8",
35
+ ),
36
+ );
37
+ } catch {
38
+ return { cli: { quickStart: [], groups: [] } };
39
+ }
40
+ })();
29
41
 
30
42
  function getPublicBaseUrl() {
31
43
  const apiBase = getApiBase();
@@ -475,8 +487,10 @@ async function showProfile() {
475
487
  }
476
488
 
477
489
  console.log(chalk.dim(' Use: tm profile set <field> <value>'));
478
- console.log(chalk.dim(' Fields: name, phone, city, country, github, linkedin, skills, bio, available'));
490
+ console.log(chalk.dim(' Fields: name, email, phone, city, country, github, linkedin, skills, bio, available'));
479
491
  console.log(chalk.dim(' telegram, whatsapp, viber, discord, teams, contact'));
492
+ console.log(chalk.dim(' Verify email: tm profile verify email [code]'));
493
+ console.log(chalk.dim(' Verify phone: tm profile verify phone [code]'));
480
494
  console.log();
481
495
  }
482
496
 
@@ -490,6 +504,7 @@ async function setProfileField(field, value) {
490
504
  // Map CLI field names to API field names
491
505
  const fieldMapping = {
492
506
  name: "name",
507
+ email: "email",
493
508
  phone: "phone",
494
509
  address: "address",
495
510
  city: "city",
@@ -530,6 +545,28 @@ async function setProfileField(field, value) {
530
545
 
531
546
  await apiPatch("/profile", { [apiField]: processedValue });
532
547
  console.log(chalk.green(`✓ Updated ${field} to "${newValue}"`));
548
+ if (field === "email") {
549
+ console.log(chalk.dim(" A verification code was sent to your new email."));
550
+ console.log(chalk.dim(" Confirm with: tm profile verify email <code>"));
551
+ }
552
+ }
553
+
554
+ async function verifyProfileChannel(channel, code) {
555
+ const normalized = String(channel || "").trim().toLowerCase();
556
+ if (!["email", "phone"].includes(normalized)) {
557
+ console.error(chalk.red("✗ Invalid channel. Use: email or phone"));
558
+ return;
559
+ }
560
+
561
+ if (!code) {
562
+ await apiPost(`/verification/send-${normalized}`, {});
563
+ console.log(chalk.green(`✓ Verification code sent for ${normalized}.`));
564
+ console.log(chalk.dim(` Confirm with: tm profile verify ${normalized} <code>`));
565
+ return;
566
+ }
567
+
568
+ await apiPost(`/verification/verify-${normalized}`, { code: String(code).trim() });
569
+ console.log(chalk.green(`✓ ${normalized[0].toUpperCase() + normalized.slice(1)} verified.`));
533
570
  }
534
571
 
535
572
  profile
@@ -546,7 +583,7 @@ profile
546
583
 
547
584
  profile
548
585
  .command("set <field> [value...]")
549
- .description("Update profile field (name, phone, address, city, country)")
586
+ .description("Update profile field (name, email, phone, address, city, country)")
550
587
  .action(async (field, value) => {
551
588
  try {
552
589
  await setProfileField(field, value);
@@ -556,6 +593,18 @@ profile
556
593
  }
557
594
  });
558
595
 
596
+ profile
597
+ .command("verify <channel> [code]")
598
+ .description("Send or confirm verification code (email|phone)")
599
+ .action(async (channel, code) => {
600
+ try {
601
+ await verifyProfileChannel(channel, code);
602
+ } catch (e) {
603
+ console.error(chalk.red(e?.message || String(e)));
604
+ process.exitCode = 1;
605
+ }
606
+ });
607
+
559
608
  profile.action(async () => {
560
609
  try {
561
610
  await showProfile();
@@ -664,34 +713,146 @@ program
664
713
  // -----------------
665
714
  program
666
715
  .command("checkout")
667
- .description("Proceed to checkout")
668
- .action(async () => {
716
+ .description("Create order from cart")
717
+ .option("--pickup", "Pick up at store")
718
+ .option("--delivery", "Delivery to address")
719
+ .option("--shipping", "Shipping (post/courier)")
720
+ .option("--phone <phone>", "Contact phone number")
721
+ .option("--address <address>", "Delivery/shipping address")
722
+ .option("-y, --yes", "Skip confirmation prompt")
723
+ .action(async (opts) => {
669
724
  try {
670
725
  const cartData = await apiGet("/cart");
671
-
672
- if (!cartData.items || cartData.items.length === 0) {
673
- console.log(chalk.yellow("Your cart is empty. Add items first."));
726
+ const items = cartData.items || [];
727
+
728
+ if (items.length === 0) {
729
+ console.log(chalk.yellow("Your cart is empty. Add items first with: tm add <product-id>"));
674
730
  return;
675
731
  }
676
-
677
- console.log(chalk.bold("Checkout"));
678
- console.log("");
679
- console.log("Your items:");
680
-
732
+
681
733
  let total = 0;
682
- cartData.items.forEach((item, i) => {
734
+ console.log();
735
+ console.log(chalk.bold(" ═══ Checkout ═══"));
736
+ console.log();
737
+ items.forEach((item, i) => {
683
738
  const name = item.name || item.product?.name || `Product #${item.productId}`;
684
739
  const price = parseFloat(item.price ?? item.product?.price ?? 0);
685
- const subtotal = price * (item.quantity || 1);
740
+ const qty = item.quantity || 1;
741
+ const subtotal = price * qty;
686
742
  total += subtotal;
687
- console.log(` ${i + 1}. ${name} x${item.quantity} = $${subtotal.toFixed(2)}`);
743
+ console.log(` ${i + 1}. ${name} x${qty} = $${subtotal.toFixed(2)}`);
688
744
  });
689
-
690
- console.log("");
691
- console.log(chalk.bold(`Total: $${total.toFixed(2)}`));
692
- console.log("");
693
- console.log(chalk.dim("To complete checkout, visit the web terminal or use:"));
694
- console.log(chalk.dim(" tm checkout --confirm"));
745
+ console.log();
746
+ console.log(chalk.bold(` Total: $${total.toFixed(2)}`));
747
+ console.log();
748
+
749
+ const hasDigitalOnly = items.length > 0 && items.every(it => {
750
+ const k = it.product?.productKind || it.productKind;
751
+ return k === "digital" || k === "saas";
752
+ });
753
+
754
+ let deliveryMethod = null;
755
+ if (opts.pickup) deliveryMethod = "pickup";
756
+ else if (opts.delivery) deliveryMethod = "delivery";
757
+ else if (opts.shipping) deliveryMethod = "shipping";
758
+
759
+ let phone = opts.phone || null;
760
+ let address = opts.address || null;
761
+
762
+ if (!hasDigitalOnly) {
763
+ // Try to pull phone/address from profile if not provided
764
+ if (!phone || !address) {
765
+ try {
766
+ const profile = await apiGet("/profile");
767
+ if (!phone && profile.phone) phone = profile.phone;
768
+ if (!address) {
769
+ const parts = [profile.address, profile.city, profile.country].filter(Boolean);
770
+ if (parts.length > 0) address = parts.join(", ");
771
+ }
772
+ } catch { /* ignore */ }
773
+ }
774
+
775
+ const inquirer = await import("inquirer").then(m => m.default);
776
+
777
+ // Ask delivery method interactively if not set
778
+ if (!deliveryMethod) {
779
+ const { method } = await inquirer.prompt([{
780
+ type: "list",
781
+ name: "method",
782
+ message: "Delivery method:",
783
+ choices: [
784
+ { name: "🏪 Pick up at store", value: "pickup" },
785
+ { name: "🚗 Delivery to address", value: "delivery" },
786
+ { name: "📦 Shipping (post/courier)", value: "shipping" },
787
+ ],
788
+ }]);
789
+ deliveryMethod = method;
790
+ }
791
+
792
+ // Ask phone if missing
793
+ if (!phone) {
794
+ const { inputPhone } = await inquirer.prompt([{
795
+ type: "input",
796
+ name: "inputPhone",
797
+ message: "Contact phone:",
798
+ validate: v => v.trim().length > 0 || "Phone is required",
799
+ }]);
800
+ phone = inputPhone.trim();
801
+ }
802
+
803
+ // Ask address if delivery/shipping and missing
804
+ if ((deliveryMethod === "delivery" || deliveryMethod === "shipping") && !address) {
805
+ const { inputAddress } = await inquirer.prompt([{
806
+ type: "input",
807
+ name: "inputAddress",
808
+ message: "Delivery address:",
809
+ validate: v => v.trim().length > 0 || "Address is required for delivery/shipping",
810
+ }]);
811
+ address = inputAddress.trim();
812
+ }
813
+ }
814
+
815
+ // Confirmation
816
+ if (!opts.yes) {
817
+ const inquirer = await import("inquirer").then(m => m.default);
818
+ console.log();
819
+ if (deliveryMethod) console.log(chalk.dim(` Method: ${deliveryMethod}`));
820
+ if (phone) console.log(chalk.dim(` Phone: ${phone}`));
821
+ if (address) console.log(chalk.dim(` Address: ${address}`));
822
+ console.log();
823
+ const { confirm } = await inquirer.prompt([{
824
+ type: "confirm",
825
+ name: "confirm",
826
+ message: `Place order for $${total.toFixed(2)}?`,
827
+ default: true,
828
+ }]);
829
+ if (!confirm) {
830
+ console.log(chalk.yellow("Order cancelled."));
831
+ return;
832
+ }
833
+ }
834
+
835
+ // Create order via API
836
+ const orderPayload = {};
837
+ if (deliveryMethod) orderPayload.deliveryMethod = deliveryMethod;
838
+ if (phone) orderPayload.phone = phone;
839
+ if (address) orderPayload.shippingAddress = address;
840
+
841
+ const result = await apiPost("/orders", orderPayload);
842
+
843
+ console.log();
844
+ console.log(chalk.green.bold(" ✓ Order created!"));
845
+ const orderId = result.order?.id || result.id;
846
+ const orderNum = result.order?.orderNumber || result.orderNumber;
847
+ if (orderNum) console.log(` Order: ${chalk.bold(orderNum)}`);
848
+ else if (orderId) console.log(` Order ID: ${chalk.bold(orderId)}`);
849
+ console.log(` Total: $${total.toFixed(2)}`);
850
+ if (deliveryMethod) console.log(` Delivery: ${deliveryMethod}`);
851
+ if (phone) console.log(` Phone: ${phone}`);
852
+ if (address) console.log(` Address: ${address}`);
853
+ console.log();
854
+ console.log(chalk.dim(" View your orders: tm orders"));
855
+ console.log();
695
856
  } catch (e) {
696
857
  console.error(chalk.red(e?.message || String(e)));
697
858
  process.exitCode = 1;
@@ -2854,39 +3015,13 @@ program
2854
3015
  // help
2855
3016
  // -----------------
2856
3017
 
2857
- // Command groups for organized help
2858
- const commandGroups = {
2859
- 'Authentication': ['login', 'logout', 'register', 'auth', 'whoami', 'profile'],
2860
- 'Shopping': ['featured', 'deals', 'products', 'search', 'view', 'buy', 'book', 'open', 'categories'],
2861
- 'Cart & Orders': ['cart', 'add', 'checkout', 'orders'],
2862
- 'Reverse Marketplace': ['request'],
2863
- 'Dev Bounties': ['live', 'need', 'ping', 'bounty', 'reputation'],
2864
- 'Automation': ['watch', 'telegram'],
2865
- 'Developer Jobs': ['jobs', 'job', 'apply', 'applications'],
2866
- 'Stores': ['sellers', 'store', 'reviews', 'where'],
2867
- 'AI Services': ['ai', 'credits', 'topup'],
2868
- 'On-Demand Tasks': ['tasks', 'task'],
2869
- 'Personalization': ['alias', 'reward'],
2870
- 'Integrations': ['apps'],
2871
- 'Info': ['about', 'stats', 'policy', 'privacy', 'faq', 'contact'],
2872
- 'System': ['start', 'doctor', 'config', 'help']
2873
- };
2874
-
2875
- // Command groups by level
2876
- const basicGroups = {
2877
- 'Get Started': ['start', 'where', 'doctor'],
2878
- 'Shop': ['featured', 'deals', 'products', 'search', 'buy', 'book', 'view', 'open'],
2879
- 'Account': ['login', 'register', 'whoami', 'profile']
2880
- };
2881
-
2882
- const advancedGroups = {
2883
- 'Cart & Orders': ['cart', 'add', 'checkout', 'orders'],
2884
- 'Reverse Marketplace': ['request'],
2885
- 'Dev Bounties': ['live', 'need', 'ping', 'bounty', 'reputation'],
2886
- 'AI Services': ['ai', 'credits', 'topup'],
2887
- 'Stores': ['sellers', 'store', 'reviews'],
2888
- 'Automation': ['watch', 'telegram', 'alias', 'reward', 'subscribe', 'wishlist', 'webhook']
2889
- };
3018
+ const cliHelpManifest = commandManifest?.cli || { quickStart: [], groups: [] };
3019
+
3020
+ function baseCommandName(commandSyntax = "") {
3021
+ if (!commandSyntax) return "";
3022
+ const clean = String(commandSyntax).trim().replace(/^tm\s+/, "");
3023
+ return clean.split(/\s+/)[0] || "";
3024
+ }
2890
3025
 
2891
3026
  // Custom help formatter
2892
3027
  function showHelp(commandName = null, mode = 'basic') {
@@ -2949,32 +3084,24 @@ function showHelp(commandName = null, mode = 'basic') {
2949
3084
  return;
2950
3085
  }
2951
3086
 
2952
- // Collect all commands
2953
- const allCommands = {};
2954
- program.commands.forEach(cmd => {
2955
- const args = (cmd.registeredArguments || []).map(a => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(' ');
2956
- allCommands[cmd.name()] = {
2957
- name: cmd.name(),
2958
- args,
2959
- desc: cmd.description()
2960
- };
2961
- });
3087
+ // Collect declared commands for existence checks
3088
+ const available = new Set(program.commands.map((cmd) => cmd.name()));
2962
3089
 
2963
- const COL_WIDTH = 28;
3090
+ const COL_WIDTH = 34;
2964
3091
 
2965
- const printGroup = (groupName, cmdNames, icon, color) => {
2966
- const groupCmds = cmdNames
2967
- .filter(name => allCommands[name])
2968
- .map(name => allCommands[name]);
3092
+ const printGroup = (group, color) => {
3093
+ const groupCmds = (group.commands || []).filter((entry) =>
3094
+ available.has(baseCommandName(entry.command)),
3095
+ );
2969
3096
 
2970
3097
  if (groupCmds.length === 0) return;
2971
3098
 
2972
- console.log(color.bold(`${icon} ${groupName}`));
2973
- groupCmds.forEach(c => {
2974
- const rawCmd = c.name + (c.args ? ' ' + c.args : '');
2975
- console.log(' ' + chalk.cyan(c.name) + (c.args ? chalk.yellow(' ' + c.args) : '') +
3099
+ console.log(color.bold(`${group.icon || '•'} ${group.title}`));
3100
+ groupCmds.forEach((entry) => {
3101
+ const rawCmd = entry.command;
3102
+ console.log(' ' + chalk.cyan(entry.command) +
2976
3103
  ' '.repeat(Math.max(1, COL_WIDTH - rawCmd.length)) +
2977
- chalk.dim(c.desc));
3104
+ chalk.dim(entry.description || ""));
2978
3105
  });
2979
3106
  console.log();
2980
3107
  };
@@ -2987,17 +3114,17 @@ function showHelp(commandName = null, mode = 'basic') {
2987
3114
  if (mode === 'basic') {
2988
3115
  // Simple, selling help
2989
3116
  console.log(chalk.yellow.bold('Quick Start:'));
2990
- console.log(` ${chalk.green('tm start')} ${chalk.dim('interactive onboarding')}`);
2991
- console.log(` ${chalk.green('tm where <city>')} ${chalk.dim('set your location')}`);
2992
- console.log(` ${chalk.green('tm featured')} ${chalk.dim('top picks this week')}`);
2993
- console.log(` ${chalk.green('tm buy <id>')} ${chalk.dim('purchase a product')}`);
3117
+ for (const step of cliHelpManifest.quickStart || []) {
3118
+ const command = step.command || "";
3119
+ console.log(` ${chalk.green(command)}${' '.repeat(Math.max(1, 28 - command.length))}${chalk.dim(step.description || "")}`);
3120
+ }
2994
3121
  console.log();
2995
3122
 
2996
- printGroup('Shop', ['featured', 'deals', 'search', 'products'], '🛒', chalk.green);
2997
- printGroup('Dev Bounties', ['live', 'need', 'ping', 'profile'], '🔥', chalk.red);
2998
- printGroup('Account', ['login', 'register', 'profile'], '👤', chalk.blue);
2999
- printGroup('Integrations', ['apps'], '🧩', chalk.magenta);
3000
- printGroup('Help', ['doctor', 'help'], '💡', chalk.gray);
3123
+ for (const group of cliHelpManifest.groups || []) {
3124
+ if (Array.isArray(group.levels) && group.levels.includes("basic")) {
3125
+ printGroup(group, chalk.white);
3126
+ }
3127
+ }
3001
3128
 
3002
3129
  console.log(chalk.dim('─'.repeat(45)));
3003
3130
  console.log(chalk.dim(' tm help --advanced') + chalk.dim(' cart, AI, rewards'));
@@ -3009,12 +3136,11 @@ function showHelp(commandName = null, mode = 'basic') {
3009
3136
  console.log(chalk.yellow.bold('Advanced Features:'));
3010
3137
  console.log();
3011
3138
 
3012
- printGroup('Cart & Orders', ['cart', 'add', 'checkout', 'orders'], '📦', chalk.yellow);
3013
- printGroup('Reverse Marketplace', ['request'], '📋', chalk.magenta);
3014
- printGroup('Dev Bounties', ['live', 'need', 'ping', 'bounty', 'reputation'], '🔥', chalk.red);
3015
- printGroup('AI Services', ['ai', 'credits', 'topup'], '🤖', chalk.cyan);
3016
- printGroup('Stores', ['sellers', 'store', 'reviews'], '🏪', chalk.magenta);
3017
- printGroup('Automation', ['watch', 'telegram', 'alias', 'reward', 'subscribe', 'wishlist'], '👁', chalk.cyan);
3139
+ for (const group of cliHelpManifest.groups || []) {
3140
+ if (Array.isArray(group.levels) && group.levels.includes("advanced")) {
3141
+ printGroup(group, chalk.white);
3142
+ }
3143
+ }
3018
3144
 
3019
3145
  console.log(chalk.dim('─'.repeat(45)));
3020
3146
  console.log(chalk.dim(' tm help') + chalk.dim(' basic commands'));
@@ -3026,42 +3152,10 @@ function showHelp(commandName = null, mode = 'basic') {
3026
3152
  console.log(chalk.magenta.bold('Usage:'), chalk.green('tm'), chalk.cyan('<command>'), chalk.dim('[options]'));
3027
3153
  console.log();
3028
3154
 
3029
- const groupColors = {
3030
- 'Authentication': chalk.blue,
3031
- 'Shopping': chalk.green,
3032
- 'Cart & Orders': chalk.yellow,
3033
- 'Reverse Marketplace': chalk.magenta,
3034
- 'Dev Bounties': chalk.red,
3035
- 'Automation': chalk.cyan,
3036
- 'Developer Jobs': chalk.magenta,
3037
- 'Stores': chalk.cyan,
3038
- 'AI Services': chalk.cyan,
3039
- 'On-Demand Tasks': chalk.yellow,
3040
- 'Integrations': chalk.magenta,
3041
- 'Personalization': chalk.white,
3042
- 'Info': chalk.dim,
3043
- 'System': chalk.gray
3044
- };
3045
-
3046
- const groupIcons = {
3047
- 'Authentication': '🔐',
3048
- 'Shopping': '🛒',
3049
- 'Cart & Orders': '📦',
3050
- 'Reverse Marketplace': '📋',
3051
- 'Dev Bounties': '🔥',
3052
- 'Automation': '👁',
3053
- 'Developer Jobs': '💼',
3054
- 'Stores': '🏪',
3055
- 'AI Services': '🤖',
3056
- 'On-Demand Tasks': '⚡',
3057
- 'Integrations': '🧩',
3058
- 'Personalization': '⚙️',
3059
- 'Info': 'ℹ️',
3060
- 'System': '💻'
3061
- };
3062
-
3063
- for (const [group, cmdNames] of Object.entries(commandGroups)) {
3064
- printGroup(group, cmdNames, groupIcons[group] || '•', groupColors[group] || chalk.white);
3155
+ for (const group of cliHelpManifest.groups || []) {
3156
+ if (Array.isArray(group.levels) && group.levels.includes("all")) {
3157
+ printGroup(group, chalk.white);
3158
+ }
3065
3159
  }
3066
3160
 
3067
3161
  console.log(chalk.dim('─'.repeat(45)));
@@ -5028,6 +5122,7 @@ function formatNamespaceResponse(namespace, command, data) {
5028
5122
  program
5029
5123
  .command("help [command]")
5030
5124
  .description("Show help for a command")
5125
+ .option("-b, --basic", "Show basic commands")
5031
5126
  .option("-a, --advanced", "Show advanced commands (cart, AI, rewards)")
5032
5127
  .option("--all", "Show all commands")
5033
5128
  .action((commandName, opts) => {
@@ -5035,6 +5130,8 @@ program
5035
5130
  showHelp(null, 'all');
5036
5131
  } else if (opts.advanced) {
5037
5132
  showHelp(null, 'advanced');
5133
+ } else if (opts.basic) {
5134
+ showHelp(null, 'basic');
5038
5135
  } else {
5039
5136
  showHelp(commandName, 'basic');
5040
5137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalmarket",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "TerminalMarket CLI — a curated marketplace for developers & founders (client for terminalmarket.app)",
5
5
  "bin": {
6
6
  "tm": "bin/tm.js"
package/src/format.js CHANGED
@@ -297,6 +297,20 @@ export function printOrders(orders) {
297
297
  orders.forEach((order) => {
298
298
  const date = new Date(order.createdAt).toLocaleDateString();
299
299
  const status = order.status?.toLowerCase() || 'pending';
300
+ const storeNames = Array.isArray(order.storeNames)
301
+ ? order.storeNames.filter(Boolean)
302
+ : [];
303
+ const itemLines = Array.isArray(order.items)
304
+ ? order.items
305
+ .map((item) => {
306
+ const name = item?.product?.name || item?.productName || `Product #${item?.productId || '?'}`;
307
+ const qty = Number(item?.quantity || 1);
308
+ const unitPrice = Number(item?.priceAtPurchase ?? item?.price ?? item?.product?.price ?? 0);
309
+ const subtotal = qty * unitPrice;
310
+ return `${name} ×${qty} (${chalk.green('$' + subtotal.toFixed(2))})`;
311
+ })
312
+ .filter(Boolean)
313
+ : [];
300
314
 
301
315
  // Status with color and icon
302
316
  let statusDisplay;
@@ -317,6 +331,12 @@ export function printOrders(orders) {
317
331
  console.log(chalk.white.bold(` ${order.orderNumber || '#' + order.id}`));
318
332
  console.log(` ${chalk.dim('Date:')} ${date} ${chalk.dim('Total:')} ${chalk.green('$' + (order.total || 0))}`);
319
333
  console.log(` ${statusDisplay}`);
334
+ if (storeNames.length > 0) {
335
+ console.log(` ${chalk.dim('Store:')} ${chalk.cyan(storeNames.join(', '))}`);
336
+ }
337
+ if (itemLines.length > 0) {
338
+ console.log(` ${chalk.dim('Items:')} ${itemLines.join(chalk.dim(' · '))}`);
339
+ }
320
340
  if (order.deliveryMethod === 'digital') {
321
341
  console.log(` ${chalk.dim('Download or key in library')}`);
322
342
  }