@layr-labs/ecloud-cli 1.0.0-devep1 → 1.0.0-devep2

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 (31) hide show
  1. package/VERSION +2 -2
  2. package/dist/commands/billing/__tests__/status.test.js +4 -23
  3. package/dist/commands/billing/__tests__/status.test.js.map +1 -1
  4. package/dist/commands/billing/__tests__/top-up.test.js +273 -89
  5. package/dist/commands/billing/__tests__/top-up.test.js.map +1 -1
  6. package/dist/commands/billing/list-cards.js +409 -0
  7. package/dist/commands/billing/list-cards.js.map +1 -0
  8. package/dist/commands/billing/status.js +4 -23
  9. package/dist/commands/billing/status.js.map +1 -1
  10. package/dist/commands/billing/top-up.js +151 -79
  11. package/dist/commands/billing/top-up.js.map +1 -1
  12. package/dist/commands/compute/app/deploy.js +1 -1
  13. package/dist/commands/compute/app/info.js +1 -1
  14. package/dist/commands/compute/app/list.js +1 -1
  15. package/dist/commands/compute/app/logs.js +1 -1
  16. package/dist/commands/compute/app/profile/set.js +1 -1
  17. package/dist/commands/compute/app/releases.js +1 -1
  18. package/dist/commands/compute/app/start.js +1 -1
  19. package/dist/commands/compute/app/stop.js +1 -1
  20. package/dist/commands/compute/app/terminate.js +1 -1
  21. package/dist/commands/compute/app/upgrade.js +1 -1
  22. package/dist/commands/compute/build/info.js +1 -1
  23. package/dist/commands/compute/build/list.js +1 -1
  24. package/dist/commands/compute/build/logs.js +1 -1
  25. package/dist/commands/compute/build/status.js +1 -1
  26. package/dist/commands/compute/build/submit.js +1 -1
  27. package/dist/commands/compute/build/verify.js +1 -1
  28. package/dist/commands/compute/undelegate.js +1 -1
  29. package/dist/hooks/init/__tests__/version-check.test.js +1 -1
  30. package/dist/hooks/init/version-check.js +1 -1
  31. package/package.json +2 -2
@@ -319,7 +319,8 @@ async function createBillingClient(flags) {
319
319
  // src/commands/billing/top-up.ts
320
320
  import { formatUnits } from "viem";
321
321
  import chalk2 from "chalk";
322
- import { input as input2 } from "@inquirer/prompts";
322
+ import { input as input2, select as select2 } from "@inquirer/prompts";
323
+ import open from "open";
323
324
 
324
325
  // src/telemetry.ts
325
326
  import {
@@ -383,12 +384,22 @@ async function withTelemetry(command, action) {
383
384
  var POLL_INTERVAL_MS = 5e3;
384
385
  var POLL_TIMEOUT_MS = 3 * 60 * 1e3;
385
386
  var BillingTopUp = class _BillingTopUp extends Command {
386
- static description = "Purchase EigenCompute credits with USDC";
387
+ static description = "Purchase EigenCompute credits with USDC or credit card";
388
+ static examples = [
389
+ "<%= config.bin %> billing top-up",
390
+ "<%= config.bin %> billing top-up --method usdc --amount 50",
391
+ "<%= config.bin %> billing top-up --method card --amount 25"
392
+ ];
387
393
  static flags = {
388
394
  ...commonFlags,
395
+ method: Flags2.string({
396
+ required: false,
397
+ description: "Payment method: usdc (on-chain) or card (credit card)",
398
+ options: ["usdc", "card"]
399
+ }),
389
400
  amount: Flags2.string({
390
401
  required: false,
391
- description: "Amount of USDC to spend (e.g., '50')"
402
+ description: "Amount to spend (USDC for on-chain, whole dollars for card)"
392
403
  }),
393
404
  account: Flags2.string({
394
405
  required: false,
@@ -424,101 +435,162 @@ ${chalk2.bold("Purchase EigenCompute credits")}`);
424
435
  const remaining = status.remainingCredits ?? 0;
425
436
  const applied = status.creditsApplied ?? 0;
426
437
  baselineTotal = remaining + applied;
427
- if (status.remainingCredits !== void 0) {
428
- this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${status.remainingCredits.toFixed(2)}`)}`);
429
- }
438
+ this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
430
439
  } catch {
431
440
  this.debug("Could not fetch current credit balance");
432
441
  }
433
- const onChainState = await billing.getTopUpInfo();
434
- const { usdcBalance, minimumPurchase } = onChainState;
435
- const balanceFormatted = formatUnits(usdcBalance, 6);
436
- this.log(` ${chalk2.bold("USDC:")} ${balanceFormatted} USDC`);
437
- if (usdcBalance === BigInt(0)) {
438
- this.log(`
442
+ const method = flags.method ?? await select2({
443
+ message: "How would you like to pay?",
444
+ choices: [
445
+ { value: "card", name: "Credit card" },
446
+ { value: "usdc", name: "USDC (on-chain)" }
447
+ ]
448
+ });
449
+ if (method === "usdc") {
450
+ await this.handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal);
451
+ } else {
452
+ await this.handleCard(billing, flags, baselineTotal);
453
+ }
454
+ });
455
+ }
456
+ async handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal) {
457
+ const onChainState = await billing.getTopUpInfo();
458
+ const { usdcBalance, minimumPurchase } = onChainState;
459
+ const balanceFormatted = formatUnits(usdcBalance, 6);
460
+ this.log(` ${chalk2.bold("USDC:")} ${balanceFormatted} USDC`);
461
+ if (usdcBalance === BigInt(0)) {
462
+ this.log(`
439
463
  ${chalk2.yellow(" No USDC in wallet.")}`);
440
- this.log(` Send USDC on Sepolia to: ${chalk2.cyan(walletAddress)}`);
441
- this.log(` Then re-run: ${chalk2.cyan("ecloud billing top-up")}
464
+ this.log(` Send USDC on Sepolia to: ${chalk2.cyan(walletAddress)}`);
465
+ this.log(` Then re-run: ${chalk2.cyan("ecloud billing top-up")}
442
466
  `);
443
- return;
467
+ return;
468
+ }
469
+ const minimumFormatted = formatUnits(minimumPurchase, 6);
470
+ const amountStr = flags.amount ?? await input2({
471
+ message: `How much USDC to spend on credits? (minimum: ${minimumFormatted})`,
472
+ validate: (val) => {
473
+ const n = parseFloat(val);
474
+ if (isNaN(n) || n <= 0) return "Enter a positive number";
475
+ const raw = BigInt(Math.round(n * 1e6));
476
+ if (raw < minimumPurchase)
477
+ return `Minimum purchase is ${minimumFormatted} USDC`;
478
+ if (raw > usdcBalance)
479
+ return `Insufficient balance. You have ${balanceFormatted} USDC`;
480
+ return true;
444
481
  }
445
- const minimumFormatted = formatUnits(minimumPurchase, 6);
446
- const amountStr = flags.amount ?? await input2({
447
- message: `How much USDC to spend on credits? (minimum: ${minimumFormatted})`,
448
- validate: (val) => {
449
- const n = parseFloat(val);
450
- if (isNaN(n) || n <= 0) return "Enter a positive number";
451
- const raw = BigInt(Math.round(n * 1e6));
452
- if (raw < minimumPurchase)
453
- return `Minimum purchase is ${minimumFormatted} USDC`;
454
- if (raw > usdcBalance)
455
- return `Insufficient balance. You have ${balanceFormatted} USDC`;
456
- return true;
457
- }
458
- });
459
- const amountFloat = parseFloat(amountStr);
460
- const amountRaw = BigInt(Math.round(amountFloat * 1e6));
461
- if (amountRaw < minimumPurchase) {
462
- this.error(`Minimum purchase is ${minimumFormatted} USDC`);
482
+ });
483
+ const amountFloat = parseFloat(amountStr);
484
+ const amountRaw = BigInt(Math.round(amountFloat * 1e6));
485
+ if (amountRaw < minimumPurchase) {
486
+ this.error(`Minimum purchase is ${minimumFormatted} USDC`);
487
+ }
488
+ if (amountRaw > usdcBalance) {
489
+ this.error(
490
+ `Insufficient USDC balance. You have ${balanceFormatted} USDC but requested ${amountFloat.toFixed(2)}`
491
+ );
492
+ }
493
+ this.log(`
494
+ Purchasing ${chalk2.bold(`$${amountFloat.toFixed(2)}`)} in credits...`);
495
+ const { txHash } = await billing.topUp({
496
+ amount: amountRaw,
497
+ account: targetAccount
498
+ });
499
+ this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${txHash}`);
500
+ await this.pollForCredits(billing, flags, baselineTotal, amountFloat);
501
+ }
502
+ async handleCard(billing, flags, baselineTotal) {
503
+ const MINIMUM_DOLLARS = 5;
504
+ const amountStr = flags.amount ?? await input2({
505
+ message: `How many dollars of credits to purchase? (minimum: $${MINIMUM_DOLLARS})`,
506
+ validate: (val) => {
507
+ const n = parseInt(val, 10);
508
+ if (isNaN(n) || n <= 0) return "Enter a positive whole number";
509
+ if (n.toString() !== val.trim()) return "Enter a whole dollar amount (no cents)";
510
+ if (n < MINIMUM_DOLLARS) return `Minimum purchase is $${MINIMUM_DOLLARS}`;
511
+ return true;
463
512
  }
464
- if (amountRaw > usdcBalance) {
465
- this.error(
466
- `Insufficient USDC balance. You have ${balanceFormatted} USDC but requested ${amountFloat.toFixed(2)}`
467
- );
513
+ });
514
+ const dollars = parseInt(amountStr, 10);
515
+ if (isNaN(dollars) || dollars < MINIMUM_DOLLARS) {
516
+ this.error(`Minimum purchase is $${MINIMUM_DOLLARS}`);
517
+ }
518
+ const amountCents = dollars * 100;
519
+ const { paymentMethods } = await billing.getPaymentMethods();
520
+ let paymentMethodId;
521
+ if (paymentMethods.length > 0) {
522
+ const choices = paymentMethods.map((card) => ({
523
+ value: card.id,
524
+ name: `${card.brand.charAt(0).toUpperCase() + card.brand.slice(1)} ending in ${card.last4}`
525
+ }));
526
+ choices.push({ value: "new", name: "Add a new card" });
527
+ const selection = await select2({
528
+ message: "Which card would you like to use?",
529
+ choices
530
+ });
531
+ if (selection !== "new") {
532
+ paymentMethodId = selection;
468
533
  }
534
+ }
535
+ this.log(`
536
+ Purchasing ${chalk2.bold(`$${dollars}`)} in credits...`);
537
+ const result = await billing.purchaseCredits(amountCents, paymentMethodId);
538
+ if (result.checkoutUrl) {
469
539
  this.log(`
470
- Purchasing ${chalk2.bold(`$${amountFloat.toFixed(2)}`)} in credits...`);
471
- const { txHash } = await billing.topUp({
472
- amount: amountRaw,
473
- account: targetAccount
474
- });
475
- this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${txHash}`);
476
- this.log(chalk2.gray("\n Waiting for credits to appear..."));
477
- const startTime = Date.now();
478
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
479
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
480
- try {
481
- const status = await billing.getStatus({
482
- productId: flags.product
483
- });
484
- const remaining = status.remainingCredits ?? 0;
485
- const applied = status.creditsApplied ?? 0;
486
- const currentTotal = remaining + applied;
487
- this.debug(`Poll: remaining=${remaining}, applied=${applied}, total=${currentTotal}, baseline=${baselineTotal}`);
488
- if (baselineTotal === void 0 || currentTotal > baselineTotal) {
489
- const creditsAdded = baselineTotal !== void 0 ? currentTotal - baselineTotal : void 0;
490
- const isMatched = creditsAdded !== void 0 && Math.abs(creditsAdded - amountFloat * 2) < 0.01;
491
- const appliedFromTopUp = creditsAdded !== void 0 ? creditsAdded - remaining : 0;
492
- this.log(`
493
- ${chalk2.green("\u2713")} Credits received: ${chalk2.cyan(`$${(creditsAdded ?? amountFloat).toFixed(2)}`)}`);
494
- if (isMatched) {
495
- this.log(` ${chalk2.green("\u2713")} Includes $${amountFloat.toFixed(2)} match bonus!`);
496
- }
497
- if (remaining > 0) {
498
- this.log(` Remaining balance: ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
499
- }
500
- if (appliedFromTopUp > 0) {
501
- this.log(` ${chalk2.gray(`$${appliedFromTopUp.toFixed(2)} applied to current bill`)}`);
502
- }
503
- this.log();
504
- return;
540
+ ${chalk2.cyan(result.checkoutUrl)}`);
541
+ this.log(chalk2.gray(" Opening checkout in browser..."));
542
+ await open(result.checkoutUrl);
543
+ } else if (result.checkoutSessionId) {
544
+ this.error(
545
+ "Checkout session created but no URL was returned. Please contact support."
546
+ );
547
+ } else {
548
+ this.log(` ${chalk2.green("\u2713")} Payment submitted`);
549
+ }
550
+ await this.pollForCredits(billing, flags, baselineTotal, dollars);
551
+ }
552
+ async pollForCredits(billing, flags, baselineTotal, amountPurchased) {
553
+ this.log(chalk2.gray("\n Waiting for credits to appear..."));
554
+ const startTime = Date.now();
555
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
556
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
557
+ try {
558
+ const status = await billing.getStatus({
559
+ productId: flags.product
560
+ });
561
+ const remaining = status.remainingCredits ?? 0;
562
+ const applied = status.creditsApplied ?? 0;
563
+ const currentTotal = remaining + applied;
564
+ this.debug(
565
+ `Poll: remaining=${remaining}, applied=${applied}, total=${currentTotal}, baseline=${baselineTotal}`
566
+ );
567
+ if (baselineTotal === void 0 || currentTotal > baselineTotal) {
568
+ const creditsAdded = baselineTotal !== void 0 ? currentTotal - baselineTotal : void 0;
569
+ this.log(
570
+ `
571
+ ${chalk2.green("\u2713")} Credits received: ${chalk2.cyan(`$${(creditsAdded ?? amountPurchased).toFixed(2)}`)}`
572
+ );
573
+ if (remaining > 0) {
574
+ this.log(` Remaining balance: ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
505
575
  }
506
- } catch {
507
- this.debug("Error polling for credit balance");
576
+ this.log();
577
+ return;
508
578
  }
579
+ } catch {
580
+ this.debug("Error polling for credit balance");
509
581
  }
510
- this.log(
511
- `
582
+ }
583
+ this.log(
584
+ `
512
585
  ${chalk2.yellow("\u26A0")} Credits haven't appeared yet. This can take a few minutes.`
513
- );
514
- this.log(` ${chalk2.gray("Check your balance:")} ecloud billing status
586
+ );
587
+ this.log(` ${chalk2.gray("Check your balance:")} ecloud billing status
515
588
  `);
516
- });
517
589
  }
518
590
  };
519
591
 
520
592
  // src/commands/billing/__tests__/top-up.test.ts
521
- import { input as input3 } from "@inquirer/prompts";
593
+ import { input as input3, select as select3 } from "@inquirer/prompts";
522
594
  vi.mock("../../../client", () => ({
523
595
  createBillingClient: vi.fn()
524
596
  }));
@@ -526,7 +598,11 @@ vi.mock("../../../telemetry", () => ({
526
598
  withTelemetry: vi.fn((_cmd, fn) => fn())
527
599
  }));
528
600
  vi.mock("@inquirer/prompts", () => ({
529
- input: vi.fn()
601
+ input: vi.fn(),
602
+ select: vi.fn()
603
+ }));
604
+ vi.mock("open", () => ({
605
+ default: vi.fn()
530
606
  }));
531
607
  var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
532
608
  var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
@@ -541,7 +617,9 @@ describe("ecloud billing top-up", () => {
541
617
  address: WALLET_ADDRESS,
542
618
  getStatus: vi.fn(),
543
619
  getTopUpInfo: vi.fn(),
544
- topUp: vi.fn()
620
+ topUp: vi.fn(),
621
+ getPaymentMethods: vi.fn(),
622
+ purchaseCredits: vi.fn()
545
623
  };
546
624
  createBillingClient.mockResolvedValue(mockBilling);
547
625
  input3.mockResolvedValue("50");
@@ -586,7 +664,7 @@ describe("ecloud billing top-up", () => {
586
664
  setupOnChainState();
587
665
  mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
588
666
  mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
589
- const cmd = createCommand({ amount: "50" });
667
+ const cmd = createCommand({ amount: "50", method: "usdc" });
590
668
  const promise = cmd.run();
591
669
  for (let i = 0; i < 10; i++) {
592
670
  await vi.advanceTimersByTimeAsync(5e3);
@@ -608,7 +686,7 @@ describe("ecloud billing top-up", () => {
608
686
  it("zero USDC balance: exits with fund wallet message", async () => {
609
687
  setupOnChainState({ usdcBalance: BigInt(0) });
610
688
  mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
611
- const cmd = createCommand({ amount: "50" });
689
+ const cmd = createCommand({ amount: "50", method: "usdc" });
612
690
  await cmd.run();
613
691
  const fullOutput = logOutput.join("\n");
614
692
  expect(fullOutput).toContain("No USDC in wallet");
@@ -619,7 +697,7 @@ describe("ecloud billing top-up", () => {
619
697
  it("below minimum purchase: shows error", async () => {
620
698
  setupOnChainState({ minimumPurchase: BigInt(1e7) });
621
699
  mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
622
- const cmd = createCommand({ amount: "5" });
700
+ const cmd = createCommand({ amount: "5", method: "usdc" });
623
701
  await expect(cmd.run()).rejects.toThrow("Minimum purchase is 10 USDC");
624
702
  });
625
703
  it("--account flag: passes different address to topUp", async () => {
@@ -627,7 +705,7 @@ describe("ecloud billing top-up", () => {
627
705
  setupOnChainState();
628
706
  mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
629
707
  mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
630
- const cmd = createCommand({ amount: "50", account: targetAccount });
708
+ const cmd = createCommand({ amount: "50", method: "usdc", account: targetAccount });
631
709
  const promise = cmd.run();
632
710
  for (let i = 0; i < 10; i++) {
633
711
  await vi.advanceTimersByTimeAsync(5e3);
@@ -647,7 +725,7 @@ describe("ecloud billing top-up", () => {
647
725
  subscriptionStatus: "active",
648
726
  remainingCredits: 10
649
727
  });
650
- const cmd = createCommand({ amount: "50" });
728
+ const cmd = createCommand({ amount: "50", method: "usdc" });
651
729
  const promise = cmd.run();
652
730
  await vi.advanceTimersByTimeAsync(2e5);
653
731
  await promise;
@@ -659,7 +737,7 @@ describe("ecloud billing top-up", () => {
659
737
  setupOnChainState();
660
738
  mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
661
739
  mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "inactive" }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 100 });
662
- const cmd = createCommand({ amount: "100" });
740
+ const cmd = createCommand({ amount: "100", method: "usdc" });
663
741
  const promise = cmd.run();
664
742
  for (let i = 0; i < 10; i++) {
665
743
  await vi.advanceTimersByTimeAsync(5e3);
@@ -671,7 +749,7 @@ describe("ecloud billing top-up", () => {
671
749
  setupOnChainState();
672
750
  mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
673
751
  mockBilling.getStatus.mockRejectedValue(new Error("API unavailable"));
674
- const cmd = createCommand({ amount: "50" });
752
+ const cmd = createCommand({ amount: "50", method: "usdc" });
675
753
  const promise = cmd.run();
676
754
  await vi.advanceTimersByTimeAsync(2e5);
677
755
  await promise;
@@ -680,5 +758,111 @@ describe("ecloud billing top-up", () => {
680
758
  expect(fullOutput).toContain("Transaction confirmed");
681
759
  expect(fullOutput).toContain("Credits haven't appeared yet");
682
760
  });
761
+ it("credit card: charges selected card on file", async () => {
762
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 35 });
763
+ mockBilling.getPaymentMethods.mockResolvedValue({
764
+ paymentMethods: [
765
+ {
766
+ id: "029641fc-3e5c-11f1-986c-5601121cbf6d",
767
+ stripePaymentMethodId: "pm_1ABC1234",
768
+ brand: "visa",
769
+ last4: "1234",
770
+ createdAt: "2026-04-20T15:00:00Z"
771
+ },
772
+ {
773
+ id: "139752fd-4e6d-22f2-a97d-6712232dcg7e",
774
+ stripePaymentMethodId: "pm_2DEF5678",
775
+ brand: "mastercard",
776
+ last4: "5678",
777
+ createdAt: "2026-04-21T10:00:00Z"
778
+ }
779
+ ]
780
+ });
781
+ mockBilling.purchaseCredits.mockResolvedValue({
782
+ purchaseId: "a1b2c3d4",
783
+ amountCents: "2500"
784
+ });
785
+ select3.mockResolvedValue("029641fc-3e5c-11f1-986c-5601121cbf6d");
786
+ const cmd = createCommand({ amount: "25", method: "card" });
787
+ const promise = cmd.run();
788
+ for (let i = 0; i < 10; i++) {
789
+ await vi.advanceTimersByTimeAsync(5e3);
790
+ }
791
+ await promise;
792
+ const fullOutput = logOutput.join("\n");
793
+ expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(2500, "029641fc-3e5c-11f1-986c-5601121cbf6d");
794
+ expect(fullOutput).toContain("Payment submitted");
795
+ expect(fullOutput).toContain("Credits received");
796
+ });
797
+ it("credit card: opens checkout when user selects add new card", async () => {
798
+ const openMock = (await import("open")).default;
799
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
800
+ mockBilling.getPaymentMethods.mockResolvedValue({
801
+ paymentMethods: [
802
+ {
803
+ id: "029641fc-3e5c-11f1-986c-5601121cbf6d",
804
+ stripePaymentMethodId: "pm_1ABC1234",
805
+ brand: "visa",
806
+ last4: "1234",
807
+ createdAt: "2026-04-20T15:00:00Z"
808
+ }
809
+ ]
810
+ });
811
+ mockBilling.purchaseCredits.mockResolvedValue({
812
+ checkoutSessionId: "cs_test_abc123",
813
+ checkoutUrl: "https://checkout.stripe.com/test",
814
+ amountCents: "2500"
815
+ });
816
+ select3.mockResolvedValue("new");
817
+ const cmd = createCommand({ amount: "25", method: "card" });
818
+ const promise = cmd.run();
819
+ await vi.advanceTimersByTimeAsync(2e5);
820
+ await promise;
821
+ const fullOutput = logOutput.join("\n");
822
+ expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(2500, void 0);
823
+ expect(openMock).toHaveBeenCalledWith("https://checkout.stripe.com/test");
824
+ expect(fullOutput).toContain("https://checkout.stripe.com/test");
825
+ });
826
+ it("credit card: opens checkout when no card on file", async () => {
827
+ const openMock = (await import("open")).default;
828
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
829
+ mockBilling.getPaymentMethods.mockResolvedValue({ paymentMethods: [] });
830
+ mockBilling.purchaseCredits.mockResolvedValue({
831
+ checkoutSessionId: "cs_test_abc123",
832
+ checkoutUrl: "https://checkout.stripe.com/test",
833
+ amountCents: "5000"
834
+ });
835
+ const cmd = createCommand({ amount: "50", method: "card" });
836
+ const promise = cmd.run();
837
+ await vi.advanceTimersByTimeAsync(2e5);
838
+ await promise;
839
+ const fullOutput = logOutput.join("\n");
840
+ expect(select3).not.toHaveBeenCalled();
841
+ expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(5e3, void 0);
842
+ expect(openMock).toHaveBeenCalledWith("https://checkout.stripe.com/test");
843
+ expect(fullOutput).toContain("https://checkout.stripe.com/test");
844
+ });
845
+ it("credit card: rejects amount below $5 minimum", async () => {
846
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
847
+ const cmd = createCommand({ amount: "3", method: "card" });
848
+ await expect(cmd.run()).rejects.toThrow("Minimum purchase is $5");
849
+ });
850
+ it("credit card: --method and --amount flags skip prompts", async () => {
851
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
852
+ mockBilling.getPaymentMethods.mockResolvedValue({ paymentMethods: [] });
853
+ mockBilling.purchaseCredits.mockResolvedValue({
854
+ checkoutSessionId: "cs_test_abc123",
855
+ checkoutUrl: "https://checkout.stripe.com/test",
856
+ amountCents: "5000"
857
+ });
858
+ const cmd = createCommand({ amount: "50", method: "card" });
859
+ const promise = cmd.run();
860
+ for (let i = 0; i < 10; i++) {
861
+ await vi.advanceTimersByTimeAsync(5e3);
862
+ }
863
+ await promise;
864
+ expect(select3).not.toHaveBeenCalled();
865
+ expect(input3).not.toHaveBeenCalled();
866
+ });
683
867
  });
684
868
  //# sourceMappingURL=top-up.test.js.map