@tarout/cli 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -1,12 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- failSpinner,
4
- registerBillingCommands,
5
- startSpinner,
6
- stopSpinner,
7
- succeedSpinner,
8
- updateSpinner
9
- } from "./chunk-BS6DFVSU.js";
10
2
  import {
11
3
  AuthError,
12
4
  BuildFailedError,
@@ -21,7 +13,7 @@ import {
21
13
  handleError,
22
14
  platformFetch,
23
15
  resetApiClient
24
- } from "./chunk-NHNK5ZQ5.js";
16
+ } from "./chunk-Y6TWR3XZ.js";
25
17
  import {
26
18
  clearConfig,
27
19
  getApiUrl,
@@ -42,7 +34,7 @@ import {
42
34
  password,
43
35
  promptOrEmit,
44
36
  select
45
- } from "./chunk-CJMIX35A.js";
37
+ } from "./chunk-DI66W4S5.js";
46
38
  import {
47
39
  ExitCode,
48
40
  box,
@@ -61,7 +53,7 @@ import {
61
53
  shouldSkipConfirmation,
62
54
  success,
63
55
  table
64
- } from "./chunk-KL3JNPAY.js";
56
+ } from "./chunk-K7JK5HIL.js";
65
57
 
66
58
  // src/index.ts
67
59
  import { Command } from "commander";
@@ -69,7 +61,7 @@ import { Command } from "commander";
69
61
  // package.json
70
62
  var package_default = {
71
63
  name: "@tarout/cli",
72
- version: "0.3.0",
64
+ version: "0.4.0",
73
65
  description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
74
66
  type: "module",
75
67
  bin: {
@@ -211,6 +203,52 @@ function isCredentialError(error2) {
211
203
  );
212
204
  }
213
205
 
206
+ // src/utils/spinner.ts
207
+ import ora from "ora";
208
+ var currentSpinner = null;
209
+ function jsonMode() {
210
+ try {
211
+ return isJsonMode();
212
+ } catch {
213
+ return false;
214
+ }
215
+ }
216
+ function startSpinner(text) {
217
+ if (jsonMode()) return null;
218
+ if (currentSpinner) {
219
+ currentSpinner.stop();
220
+ }
221
+ currentSpinner = ora(text).start();
222
+ return currentSpinner;
223
+ }
224
+ function succeedSpinner(text) {
225
+ if (jsonMode()) return;
226
+ if (currentSpinner) {
227
+ currentSpinner.succeed(text);
228
+ currentSpinner = null;
229
+ }
230
+ }
231
+ function failSpinner(text) {
232
+ if (jsonMode()) return;
233
+ if (currentSpinner) {
234
+ currentSpinner.fail(text);
235
+ currentSpinner = null;
236
+ }
237
+ }
238
+ function stopSpinner() {
239
+ if (jsonMode()) return;
240
+ if (currentSpinner) {
241
+ currentSpinner.stop();
242
+ currentSpinner = null;
243
+ }
244
+ }
245
+ function updateSpinner(text) {
246
+ if (jsonMode()) return;
247
+ if (currentSpinner) {
248
+ currentSpinner.text = text;
249
+ }
250
+ }
251
+
214
252
  // src/commands/account.ts
215
253
  function registerAccountCommands(program2) {
216
254
  const account = program2.command("account").description("Manage your user account");
@@ -594,7 +632,7 @@ Root access: ${hasAccess ? colors.success("yes") : colors.error("no")}
594
632
  try {
595
633
  if (!isLoggedIn()) throw new AuthError();
596
634
  if (!shouldSkipConfirmation()) {
597
- const { confirm: confirmFn } = await import("./prompts-QQ2FZKQT.js");
635
+ const { confirm: confirmFn } = await import("./prompts-JH6YBHHV.js");
598
636
  const ok = await confirmFn(
599
637
  `Remove user "${userId}" from the organization?`,
600
638
  false,
@@ -621,7 +659,7 @@ Root access: ${hasAccess ? colors.success("yes") : colors.error("no")}
621
659
  account.command("assign-permissions").argument("<user-id>", "User ID").option("--role <role>", "Role: admin, member").description("Assign permissions/role to a member (org owner only)").action(async (userId, options) => {
622
660
  try {
623
661
  if (!isLoggedIn()) throw new AuthError();
624
- const { select: selectFn } = await import("./prompts-QQ2FZKQT.js");
662
+ const { select: selectFn } = await import("./prompts-JH6YBHHV.js");
625
663
  const role = options.role || await selectFn(
626
664
  "Role:",
627
665
  [
@@ -1506,7 +1544,7 @@ function registerAppsCommands(program2) {
1506
1544
  throw new NotFoundError("Application", appIdentifier, suggestions);
1507
1545
  }
1508
1546
  if (!shouldSkipConfirmation()) {
1509
- const { confirm: confirm2 } = await import("./prompts-QQ2FZKQT.js");
1547
+ const { confirm: confirm2 } = await import("./prompts-JH6YBHHV.js");
1510
1548
  const confirmed = await confirm2(
1511
1549
  `Stop application "${app.name}"?`,
1512
1550
  false,
@@ -1824,7 +1862,7 @@ function registerAppsCommands(program2) {
1824
1862
  throw new NotFoundError("Application", appIdentifier);
1825
1863
  }
1826
1864
  if (!shouldSkipConfirmation()) {
1827
- const { confirm: confirm2 } = await import("./prompts-QQ2FZKQT.js");
1865
+ const { confirm: confirm2 } = await import("./prompts-JH6YBHHV.js");
1828
1866
  const confirmed = await confirm2(
1829
1867
  `Disconnect source provider from "${app.name}"?`,
1830
1868
  false,
@@ -3265,7 +3303,7 @@ function registerAuthCommands(program2) {
3265
3303
  () => input("Token name (e.g., ci-deploy):")
3266
3304
  );
3267
3305
  }
3268
- const { getApiClient: getApiClient2 } = await import("./api-QAKANRFX.js");
3306
+ const { getApiClient: getApiClient2 } = await import("./api-QVUO7EWV.js");
3269
3307
  const client = getApiClient2();
3270
3308
  const profile = getCurrentProfile();
3271
3309
  let keyOrg = profile?.organizationId;
@@ -3562,33 +3600,1062 @@ function registerBackupsCommands(program2) {
3562
3600
  handleError(err);
3563
3601
  }
3564
3602
  });
3565
- backups.command("files").argument("<destination-id>", "Backup destination ID").description("List backup files in a destination").option("-s, --search <path>", "Search path or prefix", "").action(async (destinationId, options) => {
3603
+ backups.command("files").argument("<destination-id>", "Backup destination ID").description("List backup files in a destination").option("-s, --search <path>", "Search path or prefix", "").action(async (destinationId, options) => {
3604
+ try {
3605
+ if (!isLoggedIn()) throw new AuthError();
3606
+ const client = getApiClient();
3607
+ const _spinner = startSpinner("Listing backup files...");
3608
+ const files = await client.backup.listBackupFiles.query({
3609
+ destinationId,
3610
+ search: options.search || ""
3611
+ });
3612
+ succeedSpinner();
3613
+ if (isJsonMode()) {
3614
+ outputData(files);
3615
+ return;
3616
+ }
3617
+ const list = Array.isArray(files) ? files : [];
3618
+ if (!list.length) {
3619
+ log("");
3620
+ log("No backup files found.");
3621
+ return;
3622
+ }
3623
+ log("");
3624
+ table(
3625
+ ["NAME", "SIZE", "TYPE"],
3626
+ list.map((f) => [
3627
+ f.Name || f.Path || "",
3628
+ f.IsDir ? colors.dim("DIR") : formatBytes2(f.Size || 0),
3629
+ f.IsDir ? colors.dim("directory") : "file"
3630
+ ])
3631
+ );
3632
+ log("");
3633
+ } catch (err) {
3634
+ handleError(err);
3635
+ }
3636
+ });
3637
+ backups.command("download-url").argument("<destination-id>", "Backup destination ID").argument("<backup-file>", "Backup file path").description("Get a signed download URL for a backup file").action(async (destinationId, backupFile) => {
3638
+ try {
3639
+ if (!isLoggedIn()) throw new AuthError();
3640
+ const client = getApiClient();
3641
+ const _spinner = startSpinner("Generating download URL...");
3642
+ const result = await client.backup.getBackupDownloadUrl.mutate({
3643
+ destinationId,
3644
+ backupFile
3645
+ });
3646
+ succeedSpinner();
3647
+ if (isJsonMode()) {
3648
+ outputData(result);
3649
+ return;
3650
+ }
3651
+ log("");
3652
+ log(`Download URL for ${colors.cyan(backupFile)}:`);
3653
+ log("");
3654
+ log(` ${colors.cyan(result.url || String(result))}`);
3655
+ log("");
3656
+ } catch (err) {
3657
+ handleError(err);
3658
+ }
3659
+ });
3660
+ backups.command("backup-web").argument("<backup-id>", "Backup configuration ID").description("Manually trigger a web server / app backup").action(async (backupId) => {
3661
+ try {
3662
+ if (!isLoggedIn()) throw new AuthError();
3663
+ const client = getApiClient();
3664
+ const _spinner = startSpinner("Triggering web server backup...");
3665
+ const result = await client.backup.manualBackupWebServer.mutate({
3666
+ backupId
3667
+ });
3668
+ succeedSpinner("Web server backup triggered!");
3669
+ if (isJsonMode()) outputData(result);
3670
+ else {
3671
+ log("");
3672
+ log(colors.success("Backup job started. Check logs for progress."));
3673
+ log("");
3674
+ }
3675
+ } catch (err) {
3676
+ handleError(err);
3677
+ }
3678
+ });
3679
+ backups.command("restore").argument("<backup-id>", "Backup configuration ID").argument("<backup-file>", "Backup file name to restore").description("Restore a backup with streaming log output").action(async (backupId, backupFile) => {
3680
+ try {
3681
+ if (!isLoggedIn()) throw new AuthError();
3682
+ if (!shouldSkipConfirmation()) {
3683
+ const { confirm: confirmFn } = await import("./prompts-JH6YBHHV.js");
3684
+ const ok = await confirmFn(
3685
+ `Restore backup file "${backupFile}"? This will overwrite current data.`,
3686
+ false
3687
+ );
3688
+ if (!ok) {
3689
+ log("Cancelled.");
3690
+ return;
3691
+ }
3692
+ }
3693
+ const client = getApiClient();
3694
+ const _spinner = startSpinner("Starting restore...");
3695
+ const result = await client.backup.restoreBackupWithLogs.mutate({
3696
+ backupId,
3697
+ backupFile
3698
+ });
3699
+ succeedSpinner("Restore initiated!");
3700
+ if (isJsonMode()) outputData(result);
3701
+ else {
3702
+ log("");
3703
+ log(
3704
+ colors.success("Restore job started. Monitor logs for progress.")
3705
+ );
3706
+ if (result?.jobId)
3707
+ log(` Job ID: ${colors.dim(result.jobId)}`);
3708
+ log("");
3709
+ }
3710
+ } catch (err) {
3711
+ handleError(err);
3712
+ }
3713
+ });
3714
+ }
3715
+ function formatDate3(date) {
3716
+ if (!date) return colors.dim("-");
3717
+ return new Date(date).toLocaleDateString("en-US", {
3718
+ month: "short",
3719
+ day: "numeric",
3720
+ year: "numeric"
3721
+ });
3722
+ }
3723
+ function formatBytes2(bytes) {
3724
+ if (!bytes || bytes === 0) return "0 B";
3725
+ const units = ["B", "KB", "MB", "GB", "TB"];
3726
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
3727
+ return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
3728
+ }
3729
+
3730
+ // src/commands/billing.ts
3731
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
3732
+ import open3 from "open";
3733
+
3734
+ // src/lib/billing-upgrade.ts
3735
+ function resolveTarget(input2) {
3736
+ if (input2.kind === "plan") return input2.planKey ?? "";
3737
+ if (input2.kind === "addon") {
3738
+ return input2.addonKey ?? input2.addons?.[0]?.addonKey ?? "";
3739
+ }
3740
+ return input2.planKey ?? "shared";
3741
+ }
3742
+ function chargeFromResult(result) {
3743
+ if (typeof result?.proratedChargeHalalas === "number") {
3744
+ return result.proratedChargeHalalas;
3745
+ }
3746
+ if (typeof result?.proratedCharge === "number") return result.proratedCharge;
3747
+ return void 0;
3748
+ }
3749
+ async function performBillingChange(client, input2) {
3750
+ const kind = input2.kind;
3751
+ const target = resolveTarget(input2);
3752
+ let result;
3753
+ if (kind === "plan") {
3754
+ result = await client.subscription.changePlan.mutate({
3755
+ planKey: input2.planKey,
3756
+ planQuantity: input2.quantity,
3757
+ billingPeriod: input2.billingPeriod,
3758
+ addons: input2.addons
3759
+ });
3760
+ } else if (kind === "addon") {
3761
+ const addons = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
3762
+ result = await client.subscription.purchaseAddons.mutate({ addons });
3763
+ } else {
3764
+ result = await client.subscription.setPlanQuantity.mutate({
3765
+ quantity: input2.quantity
3766
+ });
3767
+ }
3768
+ return finalizeBillingMutation(client, result, {
3769
+ kind,
3770
+ target,
3771
+ wait: input2.wait,
3772
+ timeoutMs: input2.timeoutMs,
3773
+ openBrowser: input2.openBrowser,
3774
+ onCheckoutOpened: input2.onCheckoutOpened
3775
+ });
3776
+ }
3777
+ async function finalizeBillingMutation(client, result, ctx) {
3778
+ const { kind, target } = ctx;
3779
+ const amountHalalas = chargeFromResult(result ?? {});
3780
+ if (result?.applied) {
3781
+ return {
3782
+ status: "applied",
3783
+ kind,
3784
+ target,
3785
+ amountHalalas,
3786
+ effectiveAt: result?.effectiveAt
3787
+ };
3788
+ }
3789
+ if (!result?.paymentUrl || !result?.orderId) {
3790
+ return {
3791
+ status: "deferred",
3792
+ kind,
3793
+ target,
3794
+ amountHalalas,
3795
+ effectiveAt: result?.effectiveAt
3796
+ };
3797
+ }
3798
+ const orderId = result.orderId;
3799
+ const paymentUrl = result.paymentUrl;
3800
+ if (!ctx.wait) {
3801
+ return {
3802
+ status: "payment_required",
3803
+ kind,
3804
+ target,
3805
+ orderId,
3806
+ paymentUrl,
3807
+ amountHalalas
3808
+ };
3809
+ }
3810
+ ctx.onCheckoutOpened?.({ orderId, paymentUrl });
3811
+ if (ctx.openBrowser) {
3812
+ try {
3813
+ await ctx.openBrowser(paymentUrl);
3814
+ } catch {
3815
+ }
3816
+ }
3817
+ const final = await pollCheckoutUntilTerminal(client, orderId, {
3818
+ timeoutMs: ctx.timeoutMs ?? 6e5,
3819
+ intervalMs: 4e3
3820
+ });
3821
+ const status = final.status === "PAID" ? "paid" : final.status === "FAILED" ? "failed" : final.status === "EXPIRED" ? "expired" : "pending_timeout";
3822
+ return {
3823
+ status,
3824
+ kind,
3825
+ target,
3826
+ orderId,
3827
+ paymentUrl,
3828
+ amountHalalas,
3829
+ failureReason: final.failureReason
3830
+ };
3831
+ }
3832
+ function nextCommandFor(result) {
3833
+ if ((result.status === "payment_required" || result.status === "pending_timeout") && result.orderId) {
3834
+ return `tarout billing wait ${result.orderId} --timeout 600`;
3835
+ }
3836
+ return void 0;
3837
+ }
3838
+ function formatHalalas(halalas) {
3839
+ if (typeof halalas !== "number") return void 0;
3840
+ return `${(halalas / 100).toFixed(2)} SAR`;
3841
+ }
3842
+ function emitBillingResult(result, opts) {
3843
+ const label = opts?.label ?? result.target;
3844
+ const nextCommand = nextCommandFor(result);
3845
+ const amount = formatHalalas(result.amountHalalas);
3846
+ const envelope = {
3847
+ status: result.status,
3848
+ kind: result.kind,
3849
+ target: result.target,
3850
+ ...result.orderId ? { orderId: result.orderId } : {},
3851
+ ...result.paymentUrl ? { paymentUrl: result.paymentUrl } : {},
3852
+ ...typeof result.amountHalalas === "number" ? { amountHalalas: result.amountHalalas } : {},
3853
+ ...nextCommand ? { nextCommand } : {}
3854
+ };
3855
+ switch (result.status) {
3856
+ case "applied":
3857
+ outputData({ ...envelope, hint: "Applied immediately. Retry your last action." });
3858
+ box("Plan changed", [
3859
+ `${label}: ${colors.success("applied immediately")}`,
3860
+ amount ? `Charged: ${amount}` : ""
3861
+ ].filter(Boolean));
3862
+ return ExitCode.SUCCESS;
3863
+ case "paid":
3864
+ outputData({ ...envelope, hint: "Payment confirmed. Retry your last action." });
3865
+ box("Payment confirmed", [
3866
+ `${label}: ${colors.success("subscription is active")}`,
3867
+ "Retry your last action."
3868
+ ]);
3869
+ return ExitCode.SUCCESS;
3870
+ case "deferred":
3871
+ outputData({
3872
+ ...envelope,
3873
+ hint: "Change staged for the end of the current billing period."
3874
+ });
3875
+ box("Plan change staged", [
3876
+ `${label}: will apply at the end of the current billing period.`
3877
+ ]);
3878
+ return ExitCode.SUCCESS;
3879
+ case "payment_required":
3880
+ outputData({
3881
+ ...envelope,
3882
+ hint: `Open paymentUrl to complete checkout, then run \`${nextCommand}\` \u2014 or re-run with --wait.`
3883
+ });
3884
+ box("Payment required", [
3885
+ `${label}`,
3886
+ `Open: ${colors.cyan(result.paymentUrl ?? "")}`,
3887
+ `Order ID: ${colors.dim(result.orderId ?? "")}`,
3888
+ `Then: ${colors.dim(nextCommand ?? "")}`
3889
+ ]);
3890
+ return ExitCode.SUCCESS;
3891
+ case "pending_timeout":
3892
+ outputError(
3893
+ "CHECKOUT_PENDING",
3894
+ "Hosted checkout is still pending after the timeout \u2014 resume polling or finish payment in the browser.",
3895
+ { ...envelope }
3896
+ );
3897
+ box("Still waiting", [
3898
+ `Order ID: ${colors.dim(result.orderId ?? "")}`,
3899
+ `Run: ${colors.dim(nextCommand ?? "")}`
3900
+ ]);
3901
+ return ExitCode.CHECKOUT_PENDING;
3902
+ default: {
3903
+ const code = result.status === "expired" ? "CHECKOUT_EXPIRED" : "CHECKOUT_FAILED";
3904
+ outputError(
3905
+ code,
3906
+ result.failureReason ?? `Checkout ${result.status}.`,
3907
+ { ...envelope }
3908
+ );
3909
+ box("Payment failed", [
3910
+ `Order ID: ${colors.dim(result.orderId ?? "")}`,
3911
+ `Reason: ${result.failureReason ?? result.status}`
3912
+ ]);
3913
+ return ExitCode.GENERAL_ERROR;
3914
+ }
3915
+ }
3916
+ }
3917
+ async function pollCheckoutUntilTerminal(client, orderId, opts) {
3918
+ const deadline = Date.now() + opts.timeoutMs;
3919
+ let lastStatus = "";
3920
+ while (Date.now() < deadline) {
3921
+ const r2 = await client.subscription.pollCheckoutStatus.query({ orderId });
3922
+ if (r2.status !== lastStatus) {
3923
+ if (isJsonMode()) {
3924
+ outputJsonLine({
3925
+ type: "event",
3926
+ event: "checkout_status",
3927
+ orderId,
3928
+ status: r2.status
3929
+ });
3930
+ }
3931
+ lastStatus = r2.status;
3932
+ }
3933
+ if (r2.status !== "PENDING") {
3934
+ return {
3935
+ status: r2.status,
3936
+ paidAt: r2.paidAt,
3937
+ failedAt: r2.failedAt,
3938
+ failureReason: r2.failureReason
3939
+ };
3940
+ }
3941
+ await new Promise((res) => setTimeout(res, opts.intervalMs));
3942
+ }
3943
+ const r = await client.subscription.pollCheckoutStatus.query({ orderId });
3944
+ return {
3945
+ status: r.status,
3946
+ paidAt: r.paidAt,
3947
+ failedAt: r.failedAt,
3948
+ failureReason: r.failureReason
3949
+ };
3950
+ }
3951
+
3952
+ // src/commands/billing.ts
3953
+ function reportBillingResult(result, label) {
3954
+ const code = emitBillingResult(result, { label });
3955
+ if (code !== ExitCode.SUCCESS) exit(code);
3956
+ }
3957
+ function browserOpener(noOpen) {
3958
+ if (isJsonMode() || noOpen) return void 0;
3959
+ return async (url) => {
3960
+ await open3(url);
3961
+ };
3962
+ }
3963
+ function isConflictError(err) {
3964
+ const e = err;
3965
+ return (e?.code ?? e?.data?.code) === "CONFLICT";
3966
+ }
3967
+ function registerBillingCommands(program2) {
3968
+ const billing = program2.command("billing").description("Manage subscription and billing");
3969
+ billing.command("status").description("Show current subscription and entitlements").action(async () => {
3970
+ try {
3971
+ if (!isLoggedIn()) throw new AuthError();
3972
+ const client = getApiClient();
3973
+ const _spinner = startSpinner("Fetching subscription...");
3974
+ const subscription = await client.subscription.getCurrent.query();
3975
+ succeedSpinner();
3976
+ if (isJsonMode()) {
3977
+ outputData(subscription);
3978
+ return;
3979
+ }
3980
+ log("");
3981
+ log(colors.bold("Current Subscription"));
3982
+ log("");
3983
+ if (!subscription || !subscription.planKey) {
3984
+ log(` Plan: ${colors.dim("No active subscription (free tier)")}`);
3985
+ } else {
3986
+ log(` Plan: ${colors.cyan(subscription.planKey)}`);
3987
+ if (subscription.planQuantity && subscription.planQuantity > 1) {
3988
+ log(` Quantity: ${subscription.planQuantity}`);
3989
+ }
3990
+ log(` Status: ${formatSubStatus(subscription.status || "active")}`);
3991
+ if (subscription.currentPeriodEnd) {
3992
+ log(` Renews: ${formatDate4(subscription.currentPeriodEnd)}`);
3993
+ }
3994
+ if (subscription.cancelAtPeriodEnd) {
3995
+ log(` ${colors.warn("\u26A0 Cancels at end of billing period")}`);
3996
+ }
3997
+ }
3998
+ if (subscription?.items && subscription.items.length > 0) {
3999
+ log("");
4000
+ log(colors.bold("Add-ons"));
4001
+ table(
4002
+ ["ADDON", "QUANTITY"],
4003
+ subscription.items.map((item) => [
4004
+ colors.cyan(item.addonKey || item.key || ""),
4005
+ String(item.quantity || 1)
4006
+ ])
4007
+ );
4008
+ }
4009
+ log("");
4010
+ log(`To view available plans: ${colors.dim("tarout billing plans")}`);
4011
+ } catch (err) {
4012
+ handleError(err);
4013
+ }
4014
+ });
4015
+ billing.command("plans").description("List available subscription plans").action(async () => {
4016
+ try {
4017
+ if (!isLoggedIn()) throw new AuthError();
4018
+ const client = getApiClient();
4019
+ const _spinner = startSpinner("Fetching plans...");
4020
+ const catalog = await client.subscription.getCatalog.query();
4021
+ succeedSpinner();
4022
+ if (isJsonMode()) {
4023
+ outputData(catalog);
4024
+ return;
4025
+ }
4026
+ log("");
4027
+ log(colors.bold("Available Plans"));
4028
+ log("");
4029
+ const plans = catalog?.plans || catalog || [];
4030
+ if (!Array.isArray(plans) || plans.length === 0) {
4031
+ log("No plans available.");
4032
+ return;
4033
+ }
4034
+ table(
4035
+ ["PLAN", "PRICE", "DESCRIPTION"],
4036
+ plans.map((p) => [
4037
+ colors.cyan(p.planKey || p.key || p.name || ""),
4038
+ p.priceHalalas ? `${(p.priceHalalas / 100).toFixed(2)} SAR/mo` : colors.dim("Free"),
4039
+ p.description || ""
4040
+ ])
4041
+ );
4042
+ log("");
4043
+ log(`To upgrade: ${colors.dim("tarout billing upgrade <plan>")}`);
4044
+ } catch (err) {
4045
+ handleError(err);
4046
+ }
4047
+ });
4048
+ billing.command("upgrade").argument("[plan]", "Plan key to switch to (alias: --plan)").description("Upgrade or change subscription plan").option(
4049
+ "--plan <key>",
4050
+ "Plan key (alias for the positional argument; useful for agent invocations)"
4051
+ ).option(
4052
+ "-q, --quantity <n>",
4053
+ "Plan quantity (for multi-slot plans)",
4054
+ Number.parseInt
4055
+ ).option(
4056
+ "--billing-period <period>",
4057
+ "Billing period: monthly or yearly (yearly = 10\xD7 monthly, 2 months free)",
4058
+ parseBillingPeriod
4059
+ ).option(
4060
+ "--addon <key[:qty]>",
4061
+ "Bundled addon to purchase with the plan change (repeatable, e.g. --addon db.standard:2)",
4062
+ collectAddon,
4063
+ []
4064
+ ).option(
4065
+ "-w, --wait",
4066
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4067
+ ).option(
4068
+ "--timeout <seconds>",
4069
+ "Maximum wait time in seconds (default 600)",
4070
+ (v) => Number.parseInt(v, 10),
4071
+ 600
4072
+ ).option(
4073
+ "--no-open",
4074
+ "Do not auto-open the payment URL in the default browser"
4075
+ ).action(async (planKey, options) => {
4076
+ try {
4077
+ if (!isLoggedIn()) throw new AuthError();
4078
+ const client = getApiClient();
4079
+ let targetPlan = planKey || options.plan;
4080
+ const billingPeriod = options.billingPeriod;
4081
+ const addons = Array.isArray(options.addon) && options.addon.length > 0 ? options.addon : void 0;
4082
+ if (!targetPlan) {
4083
+ const _spinner = startSpinner("Fetching plans...");
4084
+ const catalog = await client.subscription.getCatalog.query();
4085
+ succeedSpinner();
4086
+ const plans = catalog?.plans || catalog || [];
4087
+ if (!Array.isArray(plans) || plans.length === 0) {
4088
+ log("No plans available.");
4089
+ return;
4090
+ }
4091
+ targetPlan = await select(
4092
+ "Select a plan:",
4093
+ plans.map((p) => ({
4094
+ name: `${p.planKey || p.key || p.name} ${p.priceHalalas ? `(${(p.priceHalalas / 100).toFixed(2)} SAR/mo)` : "(Free)"}`,
4095
+ value: p.planKey || p.key || p.name
4096
+ })),
4097
+ {
4098
+ field: "plan",
4099
+ flag: "--plan",
4100
+ context: {
4101
+ available: plans.map((p) => ({
4102
+ key: p.planKey || p.key || p.name,
4103
+ priceHalalas: p.priceHalalas ?? 0
4104
+ }))
4105
+ }
4106
+ }
4107
+ );
4108
+ }
4109
+ if (!targetPlan) {
4110
+ throw new Error("No plan selected");
4111
+ }
4112
+ const _previewSpinner = startSpinner("Calculating change...");
4113
+ let preview;
4114
+ try {
4115
+ preview = await client.subscription.previewPlanChange.query({
4116
+ planKey: targetPlan,
4117
+ planQuantity: options.quantity,
4118
+ billingPeriod,
4119
+ addons
4120
+ });
4121
+ succeedSpinner();
4122
+ } catch {
4123
+ failSpinner();
4124
+ preview = null;
4125
+ }
4126
+ if (!shouldSkipConfirmation()) {
4127
+ log("");
4128
+ log(`Plan: ${colors.cyan(targetPlan)}`);
4129
+ if (options.quantity) log(`Quantity: ${options.quantity}`);
4130
+ if (billingPeriod) log(`Billing period: ${billingPeriod}`);
4131
+ if (addons && addons.length > 0) {
4132
+ log(
4133
+ `Addons: ${addons.map((a) => `${a.addonKey}\xD7${a.quantity}`).join(", ")}`
4134
+ );
4135
+ }
4136
+ const amountDueHalalas = typeof preview?.proratedChargeHalalas === "number" ? preview.proratedChargeHalalas : void 0;
4137
+ if (amountDueHalalas !== void 0) {
4138
+ log(
4139
+ `Amount due now: ${colors.bold(`${(amountDueHalalas / 100).toFixed(2)} SAR`)} ${colors.dim("(incl. 15% VAT for SA orgs at checkout)")}`
4140
+ );
4141
+ }
4142
+ if (typeof preview?.newPeriodTotalHalalas === "number") {
4143
+ log(
4144
+ `New recurring total: ${(preview.newPeriodTotalHalalas / 100).toFixed(2)} SAR`
4145
+ );
4146
+ }
4147
+ log("");
4148
+ const confirmed = await confirm(
4149
+ `Switch to plan "${targetPlan}"?`,
4150
+ false,
4151
+ {
4152
+ field: "confirm_upgrade",
4153
+ flag: "--yes",
4154
+ context: {
4155
+ plan: targetPlan,
4156
+ quantity: options.quantity,
4157
+ billingPeriod,
4158
+ addons,
4159
+ amountDueHalalas
4160
+ }
4161
+ }
4162
+ );
4163
+ if (!confirmed) {
4164
+ log("Cancelled.");
4165
+ return;
4166
+ }
4167
+ }
4168
+ const _changeSpinner = startSpinner("Changing plan...");
4169
+ const result = await performBillingChange(client, {
4170
+ kind: "plan",
4171
+ planKey: targetPlan,
4172
+ quantity: options.quantity,
4173
+ billingPeriod,
4174
+ addons,
4175
+ wait: options.wait,
4176
+ timeoutMs: options.timeout * 1e3,
4177
+ openBrowser: browserOpener(options.open === false),
4178
+ onCheckoutOpened: ({ orderId, paymentUrl }) => {
4179
+ if (isJsonMode()) {
4180
+ outputJsonLine({
4181
+ type: "event",
4182
+ event: "checkout_started",
4183
+ orderId,
4184
+ paymentUrl
4185
+ });
4186
+ } else {
4187
+ log("");
4188
+ log("Open this URL to complete payment:");
4189
+ log(` ${colors.cyan(paymentUrl)}`);
4190
+ log(`Order ID: ${colors.dim(orderId)}`);
4191
+ log(`Polling for confirmation (up to ${options.timeout}s)...`);
4192
+ }
4193
+ }
4194
+ });
4195
+ succeedSpinner("Plan change processed.");
4196
+ reportBillingResult(result, `Plan: ${targetPlan}`);
4197
+ } catch (err) {
4198
+ handleError(err);
4199
+ }
4200
+ });
4201
+ billing.command("confirm").argument("<orderId>", "Order ID returned by `billing upgrade`").description("Manually confirm a pending checkout (skips browser flow)").action(async (orderId) => {
4202
+ try {
4203
+ if (!isLoggedIn()) throw new AuthError();
4204
+ const client = getApiClient();
4205
+ const _s = startSpinner("Confirming checkout...");
4206
+ const result = await client.subscription.confirmCheckout.mutate({
4207
+ orderId
4208
+ });
4209
+ succeedSpinner("Checkout confirmed.");
4210
+ const mapped = {
4211
+ status: result?.applied ? "paid" : "payment_required",
4212
+ kind: "plan",
4213
+ target: orderId,
4214
+ orderId,
4215
+ ...result?.paymentUrl ? { paymentUrl: result.paymentUrl } : {}
4216
+ };
4217
+ reportBillingResult(mapped, `Order ${orderId.slice(0, 8)}`);
4218
+ } catch (err) {
4219
+ handleError(err);
4220
+ }
4221
+ });
4222
+ billing.command("wait").argument("<orderId>", "Order ID to wait on").description("Poll a pending checkout until it resolves").option(
4223
+ "--timeout <seconds>",
4224
+ "Maximum wait time in seconds (default 600)",
4225
+ (v) => Number.parseInt(v, 10),
4226
+ 600
4227
+ ).action(async (orderId, options) => {
4228
+ try {
4229
+ if (!isLoggedIn()) throw new AuthError();
4230
+ const client = getApiClient();
4231
+ if (isJsonMode()) {
4232
+ outputJsonLine({
4233
+ type: "event",
4234
+ event: "checkout_polling_started",
4235
+ orderId,
4236
+ timeoutSeconds: options.timeout
4237
+ });
4238
+ }
4239
+ const final = await pollCheckoutUntilTerminal(client, orderId, {
4240
+ timeoutMs: options.timeout * 1e3,
4241
+ intervalMs: 4e3
4242
+ });
4243
+ const result = {
4244
+ status: final.status === "PAID" ? "paid" : final.status === "FAILED" ? "failed" : final.status === "EXPIRED" ? "expired" : "pending_timeout",
4245
+ kind: "plan",
4246
+ target: orderId,
4247
+ orderId,
4248
+ failureReason: final.failureReason
4249
+ };
4250
+ reportBillingResult(result, `Order ${orderId.slice(0, 8)}`);
4251
+ } catch (err) {
4252
+ handleError(err);
4253
+ }
4254
+ });
4255
+ billing.command("cancel").description("Cancel current subscription (at period end)").action(async () => {
4256
+ try {
4257
+ if (!isLoggedIn()) throw new AuthError();
4258
+ if (!shouldSkipConfirmation()) {
4259
+ log("");
4260
+ log(
4261
+ colors.warn(
4262
+ "Cancelling your subscription will downgrade to free tier at the end of the billing period."
4263
+ )
4264
+ );
4265
+ log("");
4266
+ const confirmed = await confirm(
4267
+ "Are you sure you want to cancel your subscription?",
4268
+ false,
4269
+ { field: "confirm_cancel", flag: "--yes" }
4270
+ );
4271
+ if (!confirmed) {
4272
+ log("Cancelled.");
4273
+ return;
4274
+ }
4275
+ }
4276
+ const client = getApiClient();
4277
+ const _spinner = startSpinner("Cancelling subscription...");
4278
+ await client.subscription.cancel.mutate();
4279
+ succeedSpinner("Subscription scheduled for cancellation");
4280
+ if (isJsonMode()) {
4281
+ outputData({ cancelled: true });
4282
+ } else {
4283
+ log("");
4284
+ log(
4285
+ "Your subscription will remain active until the end of the current billing period."
4286
+ );
4287
+ log(`To undo: ${colors.dim("tarout billing resume")}`);
4288
+ log("");
4289
+ }
4290
+ } catch (err) {
4291
+ handleError(err);
4292
+ }
4293
+ });
4294
+ billing.command("resume").description("Resume a cancelled subscription").action(async () => {
4295
+ try {
4296
+ if (!isLoggedIn()) throw new AuthError();
4297
+ const client = getApiClient();
4298
+ const _spinner = startSpinner("Resuming subscription...");
4299
+ await client.subscription.resume.mutate();
4300
+ succeedSpinner("Subscription resumed!");
4301
+ if (isJsonMode()) {
4302
+ outputData({ resumed: true });
4303
+ } else {
4304
+ log("");
4305
+ log(
4306
+ colors.success(
4307
+ "Your subscription has been resumed and will continue normally."
4308
+ )
4309
+ );
4310
+ log("");
4311
+ }
4312
+ } catch (err) {
4313
+ handleError(err);
4314
+ }
4315
+ });
4316
+ billing.command("addon:add").argument("<addon>", "Addon key to add").option("-q, --quantity <n>", "Addon quantity", Number.parseInt).option(
4317
+ "-w, --wait",
4318
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4319
+ ).option(
4320
+ "--timeout <seconds>",
4321
+ "Maximum wait time in seconds (default 600)",
4322
+ (v) => Number.parseInt(v, 10),
4323
+ 600
4324
+ ).option("--no-open", "Do not auto-open the payment URL in the browser").description("Add a new addon line (extra db/storage/etc. slot)").action(async (addonKey, options) => {
4325
+ try {
4326
+ if (!isLoggedIn()) throw new AuthError();
4327
+ const quantity = options.quantity || 1;
4328
+ if (!shouldSkipConfirmation()) {
4329
+ log("");
4330
+ log(`Addon: ${colors.cyan(addonKey)}`);
4331
+ log(`Quantity: ${quantity}`);
4332
+ log("");
4333
+ const confirmed = await confirm(
4334
+ `Add addon "${addonKey}" \xD7 ${quantity}?`,
4335
+ false,
4336
+ {
4337
+ field: "confirm_addon_add",
4338
+ flag: "--yes",
4339
+ context: { addonKey, quantity }
4340
+ }
4341
+ );
4342
+ if (!confirmed) {
4343
+ log("Cancelled.");
4344
+ return;
4345
+ }
4346
+ }
4347
+ const client = getApiClient();
4348
+ const _spinner = startSpinner("Adding addon...");
4349
+ let raw;
4350
+ try {
4351
+ raw = await client.subscription.addAddon.mutate({
4352
+ addonKey,
4353
+ quantity
4354
+ });
4355
+ } catch (err) {
4356
+ failSpinner();
4357
+ if (isConflictError(err)) {
4358
+ const nextCommand = `tarout billing addon:quantity ${addonKey} <newQty>`;
4359
+ outputError(
4360
+ "ADDON_EXISTS",
4361
+ `Addon "${addonKey}" is already on your subscription \u2014 change its quantity instead of adding it again.`,
4362
+ { addonKey, nextCommand }
4363
+ );
4364
+ if (!isJsonMode()) {
4365
+ box("Addon already present", [
4366
+ `Use: ${colors.dim(nextCommand)}`,
4367
+ `Or buy more slots: ${colors.dim(`tarout billing addon:buy ${addonKey} --wait`)}`
4368
+ ]);
4369
+ }
4370
+ exit(ExitCode.INVALID_ARGUMENTS);
4371
+ return;
4372
+ }
4373
+ throw err;
4374
+ }
4375
+ succeedSpinner("Addon processed.");
4376
+ const result = await finalizeBillingMutation(client, raw, {
4377
+ kind: "addon",
4378
+ target: addonKey,
4379
+ wait: options.wait,
4380
+ timeoutMs: options.timeout * 1e3,
4381
+ openBrowser: browserOpener(options.open === false)
4382
+ });
4383
+ reportBillingResult(result, `Addon: ${addonKey} \xD7${quantity}`);
4384
+ } catch (err) {
4385
+ handleError(err);
4386
+ }
4387
+ });
4388
+ billing.command("addon:remove").argument("<addon>", "Addon key to remove").description("Remove an addon").action(async (addonKey) => {
4389
+ try {
4390
+ if (!isLoggedIn()) throw new AuthError();
4391
+ if (!shouldSkipConfirmation()) {
4392
+ const confirmed = await confirm(
4393
+ `Remove addon "${addonKey}"?`,
4394
+ false,
4395
+ {
4396
+ field: "confirm_addon_remove",
4397
+ flag: "--yes",
4398
+ context: { addonKey }
4399
+ }
4400
+ );
4401
+ if (!confirmed) {
4402
+ log("Cancelled.");
4403
+ return;
4404
+ }
4405
+ }
4406
+ const client = getApiClient();
4407
+ const _spinner = startSpinner("Removing addon...");
4408
+ await client.subscription.removeAddon.mutate({ addonKey });
4409
+ succeedSpinner("Addon removed!");
4410
+ if (isJsonMode()) {
4411
+ outputData({ removed: true, addonKey });
4412
+ }
4413
+ } catch (err) {
4414
+ handleError(err);
4415
+ }
4416
+ });
4417
+ billing.command("plan:quantity").argument("<quantity>", "New plan quantity", Number.parseInt).option(
4418
+ "-w, --wait",
4419
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4420
+ ).option(
4421
+ "--timeout <seconds>",
4422
+ "Maximum wait time in seconds (default 600)",
4423
+ (v) => Number.parseInt(v, 10),
4424
+ 600
4425
+ ).option("--no-open", "Do not auto-open the payment URL in the browser").description("Set quantity for a multi-slot plan (adds/removes app slots)").action(async (quantity, options) => {
4426
+ try {
4427
+ if (!isLoggedIn()) throw new AuthError();
4428
+ const client = getApiClient();
4429
+ const _spinner = startSpinner("Updating plan quantity...");
4430
+ const result = await performBillingChange(client, {
4431
+ kind: "plan_quantity",
4432
+ quantity,
4433
+ wait: options.wait,
4434
+ timeoutMs: options.timeout * 1e3,
4435
+ openBrowser: browserOpener(options.open === false)
4436
+ });
4437
+ succeedSpinner("Plan quantity processed.");
4438
+ reportBillingResult(result, `Plan quantity: ${quantity}`);
4439
+ } catch (err) {
4440
+ handleError(err);
4441
+ }
4442
+ });
4443
+ billing.command("addon:quantity").argument("<addon>", "Addon key").argument("<quantity>", "New quantity", Number.parseInt).description("Update quantity for an existing addon").action(async (addonKey, quantity) => {
4444
+ try {
4445
+ if (!isLoggedIn()) throw new AuthError();
4446
+ const client = getApiClient();
4447
+ const _spinner = startSpinner("Updating addon quantity...");
4448
+ const result = await client.subscription.updateAddonQuantity.mutate({
4449
+ addonKey,
4450
+ quantity
4451
+ });
4452
+ succeedSpinner("Addon quantity updated!");
4453
+ if (isJsonMode()) outputData(result);
4454
+ } catch (err) {
4455
+ handleError(err);
4456
+ }
4457
+ });
4458
+ billing.command("addon:preview").argument("<addon>", "Addon key").option("-q, --quantity <n>", "Quantity", Number.parseInt).description("Preview cost of purchasing addons").action(async (addonKey, options) => {
4459
+ try {
4460
+ if (!isLoggedIn()) throw new AuthError();
4461
+ const client = getApiClient();
4462
+ const _spinner = startSpinner("Calculating preview...");
4463
+ const preview = await client.subscription.previewAddonsPurchase.query({
4464
+ addons: [{ addonKey, quantity: options.quantity || 1 }]
4465
+ });
4466
+ succeedSpinner();
4467
+ if (isJsonMode()) {
4468
+ outputData(preview);
4469
+ return;
4470
+ }
4471
+ const p = preview;
4472
+ log("");
4473
+ log(colors.bold("Addon Purchase Preview"));
4474
+ log(` Addon: ${colors.cyan(addonKey)}`);
4475
+ log(` Quantity: ${options.quantity || 1}`);
4476
+ if (typeof p?.totalProratedHalalas === "number") {
4477
+ log(
4478
+ ` Amount Due: ${colors.bold(`${(p.totalProratedHalalas / 100).toFixed(2)} SAR`)} ${colors.dim("(incl. 15% VAT for SA orgs at checkout)")}`
4479
+ );
4480
+ }
4481
+ if (typeof p?.newPeriodTotalHalalas === "number") {
4482
+ log(
4483
+ ` New total: ${(p.newPeriodTotalHalalas / 100).toFixed(2)} SAR`
4484
+ );
4485
+ }
4486
+ log("");
4487
+ } catch (err) {
4488
+ handleError(err);
4489
+ }
4490
+ });
4491
+ billing.command("addon:buy").argument("<addon>", "Addon key").option("-q, --quantity <n>", "Quantity", Number.parseInt).option(
4492
+ "-w, --wait",
4493
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4494
+ ).option(
4495
+ "--timeout <seconds>",
4496
+ "Maximum wait time in seconds (default 600)",
4497
+ (v) => Number.parseInt(v, 10),
4498
+ 600
4499
+ ).option("--no-open", "Do not auto-open the payment URL in the browser").description("Purchase addon slots (extra db/storage/etc.)").action(async (addonKey, options) => {
4500
+ try {
4501
+ if (!isLoggedIn()) throw new AuthError();
4502
+ const quantity = options.quantity || 1;
4503
+ if (!shouldSkipConfirmation()) {
4504
+ log(`
4505
+ Purchase ${quantity}\xD7 ${colors.cyan(addonKey)}?`);
4506
+ const confirmed = await confirm("Proceed?", false, {
4507
+ field: "confirm_addon_buy",
4508
+ flag: "--yes",
4509
+ context: { addonKey, quantity }
4510
+ });
4511
+ if (!confirmed) {
4512
+ log("Cancelled.");
4513
+ return;
4514
+ }
4515
+ }
4516
+ const client = getApiClient();
4517
+ const _spinner = startSpinner("Purchasing addon...");
4518
+ const result = await performBillingChange(client, {
4519
+ kind: "addon",
4520
+ addonKey,
4521
+ quantity,
4522
+ wait: options.wait,
4523
+ timeoutMs: options.timeout * 1e3,
4524
+ openBrowser: browserOpener(options.open === false)
4525
+ });
4526
+ succeedSpinner("Addon purchase processed.");
4527
+ reportBillingResult(result, `Addon: ${addonKey} \xD7${quantity}`);
4528
+ } catch (err) {
4529
+ handleError(err);
4530
+ }
4531
+ });
4532
+ billing.command("plan:cancel-pending").description("Cancel a pending plan change (keeps current plan)").action(async () => {
4533
+ try {
4534
+ if (!isLoggedIn()) throw new AuthError();
4535
+ if (!shouldSkipConfirmation()) {
4536
+ const confirmed = await confirm(
4537
+ "Cancel the pending plan change?",
4538
+ false,
4539
+ { field: "confirm_pending_cancel", flag: "--yes" }
4540
+ );
4541
+ if (!confirmed) {
4542
+ log("Cancelled.");
4543
+ return;
4544
+ }
4545
+ }
4546
+ const client = getApiClient();
4547
+ const _spinner = startSpinner("Cancelling pending change...");
4548
+ await client.subscription.cancelPendingPlanChange.mutate();
4549
+ succeedSpinner("Pending plan change cancelled!");
4550
+ if (isJsonMode()) outputData({ cancelled: true });
4551
+ } catch (err) {
4552
+ handleError(err);
4553
+ }
4554
+ });
4555
+ billing.command("checkout:confirm").argument("<session-id>", "Checkout session ID").description("Confirm a pending checkout session").action(async (sessionId) => {
4556
+ try {
4557
+ if (!isLoggedIn()) throw new AuthError();
4558
+ const client = getApiClient();
4559
+ const _spinner = startSpinner("Confirming checkout...");
4560
+ const result = await client.subscription.confirmCheckout.mutate({
4561
+ sessionId
4562
+ });
4563
+ succeedSpinner("Checkout confirmed!");
4564
+ if (isJsonMode()) outputData(result);
4565
+ } catch (err) {
4566
+ handleError(err);
4567
+ }
4568
+ });
4569
+ billing.command("checkout:get").argument("<session-id>", "Checkout session ID").description("Get details of a checkout session").action(async (sessionId) => {
4570
+ try {
4571
+ if (!isLoggedIn()) throw new AuthError();
4572
+ const client = getApiClient();
4573
+ const _spinner = startSpinner("Fetching checkout...");
4574
+ const data = await client.payment.getCheckout.query({
4575
+ sessionId
4576
+ });
4577
+ succeedSpinner();
4578
+ if (isJsonMode()) outputData(data);
4579
+ else {
4580
+ const d = data;
4581
+ log("");
4582
+ log(colors.bold("Checkout Session"));
4583
+ log(` ID: ${d.sessionId || sessionId}`);
4584
+ log(` Status: ${d.status || "-"}`);
4585
+ log(
4586
+ ` Amount: ${d.amount !== void 0 ? `${(d.amount / 100).toFixed(2)} SAR` : "-"}`
4587
+ );
4588
+ log("");
4589
+ }
4590
+ } catch (err) {
4591
+ handleError(err);
4592
+ }
4593
+ });
4594
+ billing.command("checkout:pay").argument("<session-id>", "Checkout session ID").description("Submit card payment for a checkout session").option("--card-number <n>", "Card number").option("--exp-month <m>", "Expiry month").option("--exp-year <y>", "Expiry year").option("--cvv <cvv>", "CVV").option("--name <name>", "Cardholder name").action(async (sessionId, options) => {
3566
4595
  try {
3567
4596
  if (!isLoggedIn()) throw new AuthError();
3568
4597
  const client = getApiClient();
3569
- const _spinner = startSpinner("Listing backup files...");
3570
- const files = await client.backup.listBackupFiles.query({
3571
- destinationId,
3572
- search: options.search || ""
4598
+ const _spinner = startSpinner("Processing payment...");
4599
+ const result = await client.payment.submitCardPayment.mutate({
4600
+ sessionId,
4601
+ cardNumber: options.cardNumber,
4602
+ expMonth: options.expMonth,
4603
+ expYear: options.expYear,
4604
+ cvv: options.cvv,
4605
+ cardholderName: options.name
3573
4606
  });
4607
+ succeedSpinner("Payment submitted!");
4608
+ if (isJsonMode()) outputData(result);
4609
+ } catch (err) {
4610
+ handleError(err);
4611
+ }
4612
+ });
4613
+ billing.command("usage").description("Show resource usage breakdown").action(async () => {
4614
+ try {
4615
+ if (!isLoggedIn()) throw new AuthError();
4616
+ const client = getApiClient();
4617
+ const _spinner = startSpinner("Fetching usage...");
4618
+ const data = await client.billing.getUsageBreakdown.query();
3574
4619
  succeedSpinner();
3575
4620
  if (isJsonMode()) {
3576
- outputData(files);
4621
+ outputData(data);
3577
4622
  return;
3578
4623
  }
3579
- const list = Array.isArray(files) ? files : [];
4624
+ const d = data;
4625
+ log("");
4626
+ log(colors.bold("Usage Breakdown"));
4627
+ log(` Apps: ${d.apps ?? "-"}`);
4628
+ log(` Databases: ${d.databases ?? "-"}`);
4629
+ log(` Storage: ${d.storage ?? "-"}`);
4630
+ log(` Domains: ${d.domains ?? "-"}`);
4631
+ log("");
4632
+ } catch (err) {
4633
+ handleError(err);
4634
+ }
4635
+ });
4636
+ billing.command("resources").description("Show detailed resource usage and costs").action(async () => {
4637
+ try {
4638
+ if (!isLoggedIn()) throw new AuthError();
4639
+ const client = getApiClient();
4640
+ const _spinner = startSpinner("Fetching resource usage...");
4641
+ const data = await client.billing.getDetailedResourceUsage.query();
4642
+ succeedSpinner();
4643
+ if (isJsonMode()) {
4644
+ outputData(data);
4645
+ return;
4646
+ }
4647
+ const list = Array.isArray(data) ? data : data?.resources || [];
3580
4648
  if (!list.length) {
3581
- log("");
3582
- log("No backup files found.");
4649
+ log("\nNo active resources.\n");
3583
4650
  return;
3584
4651
  }
3585
4652
  log("");
3586
4653
  table(
3587
- ["NAME", "SIZE", "TYPE"],
3588
- list.map((f) => [
3589
- f.Name || f.Path || "",
3590
- f.IsDir ? colors.dim("DIR") : formatBytes2(f.Size || 0),
3591
- f.IsDir ? colors.dim("directory") : "file"
4654
+ ["TYPE", "NAME", "COST (SAR)"],
4655
+ list.map((r) => [
4656
+ r.type || "-",
4657
+ r.name || "-",
4658
+ r.cost !== void 0 ? (r.cost / 100).toFixed(2) : "-"
3592
4659
  ])
3593
4660
  );
3594
4661
  log("");
@@ -3596,85 +4663,132 @@ function registerBackupsCommands(program2) {
3596
4663
  handleError(err);
3597
4664
  }
3598
4665
  });
3599
- backups.command("download-url").argument("<destination-id>", "Backup destination ID").argument("<backup-file>", "Backup file path").description("Get a signed download URL for a backup file").action(async (destinationId, backupFile) => {
4666
+ billing.command("trend").description("Show daily cost trend").option("--days <n>", "Number of days", "30").action(async (options) => {
3600
4667
  try {
3601
4668
  if (!isLoggedIn()) throw new AuthError();
3602
4669
  const client = getApiClient();
3603
- const _spinner = startSpinner("Generating download URL...");
3604
- const result = await client.backup.getBackupDownloadUrl.mutate({
3605
- destinationId,
3606
- backupFile
4670
+ const _spinner = startSpinner("Fetching cost trend...");
4671
+ const data = await client.billing.getDailyCostTrend.query({
4672
+ days: Number.parseInt(options.days || "30")
3607
4673
  });
3608
4674
  succeedSpinner();
3609
4675
  if (isJsonMode()) {
3610
- outputData(result);
4676
+ outputData(data);
4677
+ return;
4678
+ }
4679
+ const list = Array.isArray(data) ? data : data?.trend || [];
4680
+ if (!list.length) {
4681
+ log("\nNo cost data found.\n");
3611
4682
  return;
3612
4683
  }
3613
4684
  log("");
3614
- log(`Download URL for ${colors.cyan(backupFile)}:`);
3615
- log("");
3616
- log(` ${colors.cyan(result.url || String(result))}`);
4685
+ log(colors.bold("Daily Cost Trend"));
4686
+ table(
4687
+ ["DATE", "COST (SAR)"],
4688
+ list.map((d) => [
4689
+ d.date || "-",
4690
+ d.cost !== void 0 ? (d.cost / 100).toFixed(2) : "-"
4691
+ ])
4692
+ );
3617
4693
  log("");
3618
4694
  } catch (err) {
3619
4695
  handleError(err);
3620
4696
  }
3621
4697
  });
3622
- backups.command("backup-web").argument("<backup-id>", "Backup configuration ID").description("Manually trigger a web server / app backup").action(async (backupId) => {
4698
+ billing.command("estimate").description("Estimate cost for a resource type").option("--type <type>", "Resource type (app, database, storage, domain)").option("--plan <plan>", "Plan key").action(async (options) => {
3623
4699
  try {
3624
4700
  if (!isLoggedIn()) throw new AuthError();
3625
4701
  const client = getApiClient();
3626
- const _spinner = startSpinner("Triggering web server backup...");
3627
- const result = await client.backup.manualBackupWebServer.mutate({
3628
- backupId
4702
+ const _spinner = startSpinner("Calculating estimate...");
4703
+ const data = await client.billing.getResourceEstimate.query({
4704
+ resourceType: options.type,
4705
+ planKey: options.plan
3629
4706
  });
3630
- succeedSpinner("Web server backup triggered!");
3631
- if (isJsonMode()) outputData(result);
3632
- else {
3633
- log("");
3634
- log(colors.success("Backup job started. Check logs for progress."));
3635
- log("");
4707
+ succeedSpinner();
4708
+ if (isJsonMode()) {
4709
+ outputData(data);
4710
+ return;
3636
4711
  }
4712
+ const d = data;
4713
+ log("");
4714
+ log(colors.bold("Resource Estimate"));
4715
+ log(
4716
+ ` Monthly: ${d.monthly !== void 0 ? `${(d.monthly / 100).toFixed(2)} SAR` : "-"}`
4717
+ );
4718
+ log(
4719
+ ` Hourly: ${d.hourly !== void 0 ? `${(d.hourly / 100).toFixed(4)} SAR` : "-"}`
4720
+ );
4721
+ log("");
3637
4722
  } catch (err) {
3638
4723
  handleError(err);
3639
4724
  }
3640
4725
  });
3641
- backups.command("restore").argument("<backup-id>", "Backup configuration ID").argument("<backup-file>", "Backup file name to restore").description("Restore a backup with streaming log output").action(async (backupId, backupFile) => {
4726
+ billing.command("active-resources").description("List all active billable resources").action(async () => {
3642
4727
  try {
3643
4728
  if (!isLoggedIn()) throw new AuthError();
3644
- if (!shouldSkipConfirmation()) {
3645
- const { confirm: confirmFn } = await import("./prompts-QQ2FZKQT.js");
3646
- const ok = await confirmFn(
3647
- `Restore backup file "${backupFile}"? This will overwrite current data.`,
3648
- false
3649
- );
3650
- if (!ok) {
3651
- log("Cancelled.");
3652
- return;
3653
- }
3654
- }
3655
4729
  const client = getApiClient();
3656
- const _spinner = startSpinner("Starting restore...");
3657
- const result = await client.backup.restoreBackupWithLogs.mutate({
3658
- backupId,
3659
- backupFile
3660
- });
3661
- succeedSpinner("Restore initiated!");
3662
- if (isJsonMode()) outputData(result);
3663
- else {
3664
- log("");
3665
- log(
3666
- colors.success("Restore job started. Monitor logs for progress.")
3667
- );
3668
- if (result?.jobId)
3669
- log(` Job ID: ${colors.dim(result.jobId)}`);
3670
- log("");
4730
+ const _spinner = startSpinner("Fetching active resources...");
4731
+ const data = await client.billing.getActiveResources.query();
4732
+ succeedSpinner();
4733
+ if (isJsonMode()) {
4734
+ outputData(data);
4735
+ return;
4736
+ }
4737
+ const list = Array.isArray(data) ? data : data?.resources || [];
4738
+ if (!list.length) {
4739
+ log("\nNo active billable resources.\n");
4740
+ return;
3671
4741
  }
4742
+ log("");
4743
+ table(
4744
+ ["TYPE", "NAME", "STATUS", "PLAN"],
4745
+ list.map((r) => [
4746
+ r.type || "-",
4747
+ r.name || "-",
4748
+ r.status || "-",
4749
+ r.plan || r.planKey || "-"
4750
+ ])
4751
+ );
4752
+ log("");
3672
4753
  } catch (err) {
3673
4754
  handleError(err);
3674
4755
  }
3675
4756
  });
3676
4757
  }
3677
- function formatDate3(date) {
4758
+ function collectAddon(value, previous) {
4759
+ const [rawKey, rawQty] = value.split(":");
4760
+ const addonKey = (rawKey || "").trim();
4761
+ if (!addonKey) {
4762
+ throw new InvalidArgumentError2(
4763
+ `Invalid --addon value "${value}". Expected key[:qty] (e.g. db.standard:2).`
4764
+ );
4765
+ }
4766
+ const quantity = rawQty === void 0 ? 1 : Number.parseInt(rawQty, 10);
4767
+ if (!Number.isFinite(quantity) || quantity <= 0) {
4768
+ throw new InvalidArgumentError2(
4769
+ `Invalid --addon quantity in "${value}". Expected a positive integer.`
4770
+ );
4771
+ }
4772
+ return [...previous, { addonKey, quantity }];
4773
+ }
4774
+ function parseBillingPeriod(raw) {
4775
+ const v = raw.toLowerCase();
4776
+ if (v === "monthly" || v === "yearly") return v;
4777
+ throw new InvalidArgumentError2(
4778
+ `Invalid --billing-period "${raw}". Expected "monthly" or "yearly".`
4779
+ );
4780
+ }
4781
+ function formatSubStatus(status) {
4782
+ const map = {
4783
+ active: colors.success("active"),
4784
+ trialing: colors.info("trialing"),
4785
+ past_due: colors.warn("past due"),
4786
+ cancelled: colors.error("cancelled"),
4787
+ none: colors.dim("none")
4788
+ };
4789
+ return map[status] || status;
4790
+ }
4791
+ function formatDate4(date) {
3678
4792
  if (!date) return colors.dim("-");
3679
4793
  return new Date(date).toLocaleDateString("en-US", {
3680
4794
  month: "short",
@@ -3682,12 +4796,6 @@ function formatDate3(date) {
3682
4796
  year: "numeric"
3683
4797
  });
3684
4798
  }
3685
- function formatBytes2(bytes) {
3686
- if (!bytes || bytes === 0) return "0 B";
3687
- const units = ["B", "KB", "MB", "GB", "TB"];
3688
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
3689
- return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
3690
- }
3691
4799
 
3692
4800
  // src/lib/process.ts
3693
4801
  import { spawn } from "child_process";
@@ -4370,7 +5478,7 @@ function registerDbCommands(program2) {
4370
5478
  db2.name,
4371
5479
  getTypeLabel(db2.type),
4372
5480
  getStatusBadge(db2.status),
4373
- formatDate4(db2.created)
5481
+ formatDate5(db2.created)
4374
5482
  ])
4375
5483
  );
4376
5484
  log("");
@@ -5226,7 +6334,7 @@ function findDatabase(databases, identifier) {
5226
6334
  function generateSlug2(name) {
5227
6335
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
5228
6336
  }
5229
- function formatDate4(date) {
6337
+ function formatDate5(date) {
5230
6338
  const d = new Date(date);
5231
6339
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
5232
6340
  }
@@ -5312,7 +6420,78 @@ import {
5312
6420
  import { tmpdir } from "os";
5313
6421
  import { basename, dirname, join as join3 } from "path";
5314
6422
  import { promisify } from "util";
5315
- import open3 from "open";
6423
+ import open4 from "open";
6424
+
6425
+ // src/lib/entitlement-remedy.ts
6426
+ var planKeyOf = (p) => p.planKey ?? p.key ?? "";
6427
+ var addonKeyOf = (a) => a.addonKey ?? a.key ?? "";
6428
+ function nextPlanForRequested(requested) {
6429
+ const r = (requested ?? "free").trim().toLowerCase();
6430
+ if (!r || r === "free") return "shared";
6431
+ if (r === "shared") return "shared";
6432
+ if (r === "dedicated" || r === "dedicated_small") return "dedicated_small";
6433
+ if (r === "dedicated_medium") return "dedicated_medium";
6434
+ if (r === "dedicated_large") return "dedicated_large";
6435
+ return r;
6436
+ }
6437
+ function resolveEntitlementRemedy(failedKey, catalog, opts) {
6438
+ const plans = catalog?.plans ?? [];
6439
+ const addons = catalog?.addons ?? [];
6440
+ if (failedKey?.startsWith("app.shared")) {
6441
+ const sharedPlan = plans.find((p) => p.quantityAware);
6442
+ const targetKey = sharedPlan ? planKeyOf(sharedPlan) : "shared";
6443
+ const next = (opts?.currentSharedQuantity ?? 1) + 1;
6444
+ return {
6445
+ kind: "plan_quantity",
6446
+ failedKey,
6447
+ targetKey,
6448
+ targetName: sharedPlan?.name,
6449
+ priceHalalas: sharedPlan?.priceHalalas,
6450
+ command: `tarout billing plan:quantity ${next} --wait`,
6451
+ hint: `Your ${sharedPlan?.name ?? "Starter"} plan is quantity-aware \u2014 add an app slot by raising the plan quantity to ${next}.`
6452
+ };
6453
+ }
6454
+ if (failedKey?.startsWith("app.dedicated") || failedKey?.startsWith("host.")) {
6455
+ const requested = opts?.requestedPlan;
6456
+ const target2 = requested && requested.toLowerCase().startsWith("dedicated") ? nextPlanForRequested(requested) : "dedicated_small";
6457
+ const plan2 = plans.find((p) => planKeyOf(p) === target2);
6458
+ return {
6459
+ kind: "plan",
6460
+ failedKey,
6461
+ targetKey: target2,
6462
+ targetName: plan2?.name,
6463
+ priceHalalas: plan2?.priceHalalas,
6464
+ command: `tarout billing upgrade ${target2} --wait`,
6465
+ hint: `A dedicated host slot is required \u2014 upgrade to ${plan2?.name ?? target2}.`
6466
+ };
6467
+ }
6468
+ if (failedKey?.startsWith("db.") || failedKey?.startsWith("storage.") || failedKey?.startsWith("domain") || failedKey?.startsWith("email")) {
6469
+ const matched = addons.find(
6470
+ (a) => a.grants?.some((g) => g.entitlementKey === failedKey)
6471
+ ) ?? addons.find((a) => addonKeyOf(a) === failedKey.replace(/\.slots$/, ""));
6472
+ const targetKey = matched ? addonKeyOf(matched) : failedKey.replace(/\.slots$/, "");
6473
+ return {
6474
+ kind: "addon",
6475
+ failedKey,
6476
+ targetKey,
6477
+ targetName: matched?.name,
6478
+ priceHalalas: matched?.priceHalalas,
6479
+ command: `tarout billing addon:buy ${targetKey} --wait`,
6480
+ hint: `Add a ${matched?.name ?? targetKey} slot with an addon purchase.`
6481
+ };
6482
+ }
6483
+ const target = nextPlanForRequested(opts?.requestedPlan);
6484
+ const plan = plans.find((p) => planKeyOf(p) === target);
6485
+ return {
6486
+ kind: "plan",
6487
+ failedKey,
6488
+ targetKey: target,
6489
+ targetName: plan?.name,
6490
+ priceHalalas: plan?.priceHalalas,
6491
+ command: `tarout billing upgrade ${target} --wait`,
6492
+ hint: `Upgrade to a paid plan (${plan?.name ?? target}) to continue.`
6493
+ };
6494
+ }
5316
6495
 
5317
6496
  // src/lib/websocket.ts
5318
6497
  import WebSocket from "ws";
@@ -5477,7 +6656,7 @@ async function authenticateViaBrowser(action, apiUrl) {
5477
6656
  log(
5478
6657
  action === "register" ? "Opening browser to create your account..." : "Opening browser to authenticate..."
5479
6658
  );
5480
- await open3(authUrl);
6659
+ await open4(authUrl);
5481
6660
  const _spinner = startSpinner(
5482
6661
  action === "register" ? "Waiting for account creation..." : "Waiting for authentication..."
5483
6662
  );
@@ -6093,48 +7272,37 @@ function formatPlanPrice(priceHalalas) {
6093
7272
  return `${(priceHalalas / 100).toFixed(2)} SAR/mo`;
6094
7273
  }
6095
7274
  async function runInlineUpgrade(client, planKey) {
6096
- const { pollCheckoutUntilTerminal } = await import("./billing-GUA4S2Y4.js");
6097
7275
  const _changing = startSpinner(`Switching to plan "${planKey}"...`);
6098
- const result = await client.subscription.changePlan.mutate({ planKey });
6099
- if (result?.applied) {
6100
- succeedSpinner("Plan applied.");
6101
- return true;
6102
- }
6103
- if (!result?.paymentUrl || !result?.orderId) {
6104
- failSpinner("Plan change did not return a payment URL.");
6105
- return false;
6106
- }
6107
- succeedSpinner("Checkout opened \u2014 waiting for payment confirmation.");
6108
- const orderId = result.orderId;
6109
- const paymentUrl = result.paymentUrl;
6110
- log("");
6111
- log(`Open this URL to complete payment:`);
6112
- log(` ${colors.cyan(paymentUrl)}`);
6113
- log(`Order ID: ${colors.dim(orderId)}`);
6114
- try {
6115
- await open3(paymentUrl);
6116
- } catch {
6117
- }
6118
- const _pollSpinner = startSpinner("Polling for payment confirmation...");
6119
- const final = await pollCheckoutUntilTerminal(client, orderId, {
7276
+ const result = await performBillingChange(client, {
7277
+ kind: "plan",
7278
+ planKey,
7279
+ wait: true,
6120
7280
  timeoutMs: 6e5,
6121
- intervalMs: 4e3
7281
+ openBrowser: isJsonMode() ? void 0 : async (url) => {
7282
+ await open4(url);
7283
+ },
7284
+ onCheckoutOpened: ({ orderId, paymentUrl }) => {
7285
+ log("");
7286
+ log("Open this URL to complete payment:");
7287
+ log(` ${colors.cyan(paymentUrl)}`);
7288
+ log(`Order ID: ${colors.dim(orderId)}`);
7289
+ }
6122
7290
  });
6123
- if (final.status === "PAID") {
6124
- succeedSpinner("Payment confirmed.");
7291
+ if (result.status === "applied" || result.status === "paid") {
7292
+ succeedSpinner("Plan applied.");
6125
7293
  return true;
6126
7294
  }
6127
7295
  failSpinner(
6128
- final.status === "PENDING" ? "Payment still pending after 10 minutes." : `Payment ${final.status.toLowerCase()}.`
7296
+ result.status === "deferred" ? "Plan change did not require an immediate payment." : result.status === "pending_timeout" ? "Payment still pending after 10 minutes." : `Payment ${result.status}.`
6129
7297
  );
6130
- if (final.failureReason) {
6131
- log(colors.error(final.failureReason));
7298
+ if (result.failureReason) log(colors.error(result.failureReason));
7299
+ if (result.orderId) {
7300
+ log(
7301
+ colors.dim(
7302
+ `Resume later with: tarout billing wait ${result.orderId.slice(0, 8)} --timeout 600`
7303
+ )
7304
+ );
6132
7305
  }
6133
- log(
6134
- colors.dim(
6135
- `Resume later with: tarout billing wait ${orderId.slice(0, 8)} --timeout 600`
6136
- )
6137
- );
6138
7306
  return false;
6139
7307
  }
6140
7308
  async function getAppPlanChoices(client, preloadedOptions) {
@@ -6189,13 +7357,7 @@ function isEntitlementError(err) {
6189
7357
  return msg.includes("plan limit reached") || msg.includes("entitlement") || msg.includes("upgrade to add more") || msg.includes("active subscription") || msg.includes("free_not_allowed_on_paid_plan");
6190
7358
  }
6191
7359
  function inferSuggestedPlan(requested) {
6192
- const r = (requested ?? "free").trim().toLowerCase();
6193
- if (!r || r === "free") return "shared";
6194
- if (r === "shared") return "shared";
6195
- if (r === "dedicated" || r === "dedicated_small") return "dedicated_small";
6196
- if (r === "dedicated_medium") return "dedicated_medium";
6197
- if (r === "dedicated_large") return "dedicated_large";
6198
- return r;
7360
+ return nextPlanForRequested(requested);
6199
7361
  }
6200
7362
  var ENTITLEMENT_LABELS = {
6201
7363
  "app.free.slots": "Free app slot",
@@ -6233,6 +7395,22 @@ function extractEntitlementKeyFromError(err) {
6233
7395
  const m = msg.match(/Plan limit reached for ([\w.]+)/i);
6234
7396
  return m?.[1];
6235
7397
  }
7398
+ async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
7399
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
7400
+ const failedKey = extractEntitlementKeyFromError(err);
7401
+ const catalog = await fetchCatalogSafely(client);
7402
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
7403
+ outputError("NEEDS_UPGRADE", message, {
7404
+ failedEntitlementKey: failedKey,
7405
+ remedyKind: remedy.kind,
7406
+ // `suggestedPlan` retained for back-compat; now correct for every gate
7407
+ // (the plan key, addon key, or quantity-aware plan to act on).
7408
+ suggestedPlan: remedy.targetKey,
7409
+ suggestedTarget: remedy.targetKey,
7410
+ nextCommand: remedy.command,
7411
+ hint: `${remedy.hint} Then retry: ${retryCommand}.`
7412
+ });
7413
+ }
6236
7414
  async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
6237
7415
  const catalog = await fetchCatalogSafely(client);
6238
7416
  const allPlans = catalog?.plans ?? [];
@@ -6376,7 +7554,7 @@ async function openGitProviderSetup() {
6376
7554
  log(
6377
7555
  `No GitHub account is required if you keep using ${colors.dim("--source upload")}.`
6378
7556
  );
6379
- await open3(url);
7557
+ await open4(url);
6380
7558
  }
6381
7559
  async function configureOptionalResources(client, profile, app, options, inspection) {
6382
7560
  const database = await resolveDatabaseChoice(options, inspection);
@@ -6787,28 +7965,58 @@ function resolveExplicitStorageRef(candidates, ref) {
6787
7965
  }
6788
7966
  return match;
6789
7967
  }
7968
+ function emitReuseNotice(payload) {
7969
+ if (isJsonMode()) {
7970
+ outputJsonLine({ type: "event", event: payload.event, ...payload.details });
7971
+ return;
7972
+ }
7973
+ log(colors.warn(payload.humanMessage));
7974
+ if (payload.humanHint) log(colors.dim(` ${payload.humanHint}`));
7975
+ }
6790
7976
  async function finalizeDatabaseReuse(client, candidate, kind, requestedPlan, planExplicit) {
6791
7977
  let plan = candidate.plan;
6792
7978
  let upgraded = false;
6793
7979
  if (planExplicit && requestedPlan) {
6794
7980
  const cmp = compareResourcePlan(requestedPlan, candidate.plan);
7981
+ const upgradeCommand = `tarout db upgrade ${candidate.name} --plan ${requestedPlan.toLowerCase()}`;
6795
7982
  if (cmp > 0) {
6796
- const ok = await confirmUpgrade(
6797
- `${formatDatabaseKind(kind)} ${candidate.name} is on ${candidate.plan}. Upgrade to ${requestedPlan} before attaching?`,
6798
- true,
6799
- kind === "postgres" ? "upgrade_postgres" : "upgrade_mysql"
6800
- );
6801
- if (ok) {
6802
- await runDatabaseUpgrade(client, kind, candidate.id, requestedPlan);
6803
- plan = requestedPlan;
6804
- upgraded = true;
7983
+ if (isJsonMode() || shouldSkipConfirmation()) {
7984
+ emitReuseNotice({
7985
+ event: "reuse_plan_unchanged",
7986
+ humanMessage: `${formatDatabaseKind(kind)} ${candidate.name} stays on ${candidate.plan}; requested ${requestedPlan} was not applied automatically.`,
7987
+ humanHint: `Upgrade explicitly with: ${upgradeCommand}`,
7988
+ details: {
7989
+ resourceType: "database",
7990
+ name: candidate.name,
7991
+ currentPlan: candidate.plan,
7992
+ requestedPlan,
7993
+ nextCommand: upgradeCommand,
7994
+ hint: "Database plan upgrades are billing-mutating and may require checkout; run the command above to upgrade."
7995
+ }
7996
+ });
7997
+ } else {
7998
+ const ok = await confirmUpgrade(
7999
+ `${formatDatabaseKind(kind)} ${candidate.name} is on ${candidate.plan}. Upgrade to ${requestedPlan} before attaching?`,
8000
+ true,
8001
+ kind === "postgres" ? "upgrade_postgres" : "upgrade_mysql"
8002
+ );
8003
+ if (ok) {
8004
+ await runDatabaseUpgrade(client, kind, candidate.id, requestedPlan);
8005
+ plan = requestedPlan;
8006
+ upgraded = true;
8007
+ }
6805
8008
  }
6806
- } else if (cmp < 0 && !isJsonMode()) {
6807
- log(
6808
- colors.warn(
6809
- `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`
6810
- )
6811
- );
8009
+ } else if (cmp < 0) {
8010
+ emitReuseNotice({
8011
+ event: "reuse_plan_lower",
8012
+ humanMessage: `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`,
8013
+ details: {
8014
+ resourceType: "database",
8015
+ name: candidate.name,
8016
+ currentPlan: candidate.plan,
8017
+ requestedPlan
8018
+ }
8019
+ });
6812
8020
  }
6813
8021
  }
6814
8022
  return {
@@ -6824,16 +8032,10 @@ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplic
6824
8032
  let upgraded = false;
6825
8033
  if (planExplicit && requestedPlan) {
6826
8034
  const cmp = compareResourcePlan(requestedPlan, candidate.plan);
8035
+ const upgradeCommand = `tarout storage upgrade ${candidate.name} --plan ${requestedPlan.toLowerCase()}`;
6827
8036
  if (cmp > 0) {
6828
- if (candidate.plan !== "FREE") {
6829
- if (!isJsonMode()) {
6830
- log(
6831
- colors.warn(
6832
- `Storage bucket ${candidate.name} is on ${candidate.plan}. Inline upgrades to ${requestedPlan} are only supported from FREE \u2014 upgrade from the dashboard. Attaching at current plan.`
6833
- )
6834
- );
6835
- }
6836
- } else if (requestedPlan === "STARTER" || requestedPlan === "STANDARD" || requestedPlan === "PRO") {
8037
+ const isPaidUpgradeable = requestedPlan === "STARTER" || requestedPlan === "STANDARD" || requestedPlan === "PRO";
8038
+ if (candidate.plan === "FREE" && isPaidUpgradeable && !isJsonMode() && !shouldSkipConfirmation()) {
6837
8039
  const ok = await confirmUpgrade(
6838
8040
  `Storage bucket ${candidate.name} is on FREE. Upgrade to ${requestedPlan} before attaching?`,
6839
8041
  true,
@@ -6844,13 +8046,32 @@ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplic
6844
8046
  plan = requestedPlan;
6845
8047
  upgraded = true;
6846
8048
  }
8049
+ } else {
8050
+ emitReuseNotice({
8051
+ event: "reuse_plan_unchanged",
8052
+ humanMessage: `Storage bucket ${candidate.name} stays on ${candidate.plan}; requested ${requestedPlan} was not applied automatically.`,
8053
+ humanHint: `Upgrade explicitly with: ${upgradeCommand}`,
8054
+ details: {
8055
+ resourceType: "storage",
8056
+ name: candidate.name,
8057
+ currentPlan: candidate.plan,
8058
+ requestedPlan,
8059
+ nextCommand: upgradeCommand,
8060
+ hint: "Storage plan upgrades are billing-mutating; run the command above to upgrade."
8061
+ }
8062
+ });
6847
8063
  }
6848
- } else if (cmp < 0 && !isJsonMode()) {
6849
- log(
6850
- colors.warn(
6851
- `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`
6852
- )
6853
- );
8064
+ } else if (cmp < 0) {
8065
+ emitReuseNotice({
8066
+ event: "reuse_plan_lower",
8067
+ humanMessage: `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`,
8068
+ details: {
8069
+ resourceType: "storage",
8070
+ name: candidate.name,
8071
+ currentPlan: candidate.plan,
8072
+ requestedPlan
8073
+ }
8074
+ });
6854
8075
  }
6855
8076
  }
6856
8077
  return {
@@ -6862,9 +8083,6 @@ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplic
6862
8083
  };
6863
8084
  }
6864
8085
  async function confirmUpgrade(message, defaultValue, field) {
6865
- if (isJsonMode() || shouldSkipConfirmation()) {
6866
- return false;
6867
- }
6868
8086
  return confirm(message, defaultValue, {
6869
8087
  field,
6870
8088
  flag: "--yes (default attaches at current plan; pass --database-plan/--storage-plan with --yes to force an upgrade)"
@@ -7301,11 +8519,12 @@ function registerDeployCommands(program2) {
7301
8519
  if (isEntitlementError(err)) {
7302
8520
  const message = err instanceof Error ? err.message : "Plan upgrade required";
7303
8521
  if (isJsonMode() || shouldSkipConfirmation()) {
7304
- outputError("NEEDS_UPGRADE", message, {
7305
- suggestedPlan: inferSuggestedPlan(options.plan),
7306
- failedEntitlementKey: extractEntitlementKeyFromError(err),
7307
- hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout deploy`."
7308
- });
8522
+ await emitNeedsUpgrade(
8523
+ getApiClient(),
8524
+ err,
8525
+ options.plan,
8526
+ "tarout deploy"
8527
+ );
7309
8528
  exit(ExitCode.PERMISSION_DENIED);
7310
8529
  }
7311
8530
  log("");
@@ -7316,11 +8535,12 @@ function registerDeployCommands(program2) {
7316
8535
  options.plan
7317
8536
  );
7318
8537
  if (!upgraded) {
7319
- outputError("NEEDS_UPGRADE", message, {
7320
- suggestedPlan: inferSuggestedPlan(options.plan),
7321
- failedEntitlementKey: extractEntitlementKeyFromError(err),
7322
- hint: "Run `tarout billing upgrade <plan> --wait`, then retry `tarout deploy`."
7323
- });
8538
+ await emitNeedsUpgrade(
8539
+ getApiClient(),
8540
+ err,
8541
+ options.plan,
8542
+ "tarout deploy"
8543
+ );
7324
8544
  exit(ExitCode.PERMISSION_DENIED);
7325
8545
  }
7326
8546
  box("Upgrade complete", [
@@ -7451,7 +8671,7 @@ function registerDeployCommands(program2) {
7451
8671
  colors.cyan(d.deploymentId.slice(0, 8)),
7452
8672
  getStatusBadge(d.status),
7453
8673
  d.title || colors.dim("-"),
7454
- formatDate5(d.createdAt)
8674
+ formatDate6(d.createdAt)
7455
8675
  ])
7456
8676
  );
7457
8677
  log("");
@@ -7643,10 +8863,10 @@ function registerDeployCommands(program2) {
7643
8863
  );
7644
8864
  log("");
7645
8865
  const choices = successfulDeployments.slice(0, 10).map((d, index) => ({
7646
- name: `${colors.cyan(d.deploymentId.slice(0, 8))} - ${d.title || "Deployment"} (${formatDate5(d.createdAt)})${index === 0 ? colors.dim(" [current]") : ""}`,
8866
+ name: `${colors.cyan(d.deploymentId.slice(0, 8))} - ${d.title || "Deployment"} (${formatDate6(d.createdAt)})${index === 0 ? colors.dim(" [current]") : ""}`,
7647
8867
  value: d.deploymentId
7648
8868
  }));
7649
- const { select: select2 } = await import("./prompts-QQ2FZKQT.js");
8869
+ const { select: select2 } = await import("./prompts-JH6YBHHV.js");
7650
8870
  targetDeploymentId = await select2(
7651
8871
  "Select deployment:",
7652
8872
  choices,
@@ -7662,10 +8882,10 @@ function registerDeployCommands(program2) {
7662
8882
  log(` Deployment: ${colors.cyan(targetDeploymentId.slice(0, 8))}`);
7663
8883
  log(` Title: ${targetDeployment?.title || "Deployment"}`);
7664
8884
  log(
7665
- ` Created: ${targetDeployment ? formatDate5(targetDeployment.createdAt) : colors.dim("unknown")}`
8885
+ ` Created: ${targetDeployment ? formatDate6(targetDeployment.createdAt) : colors.dim("unknown")}`
7666
8886
  );
7667
8887
  log("");
7668
- const { confirm: confirm2 } = await import("./prompts-QQ2FZKQT.js");
8888
+ const { confirm: confirm2 } = await import("./prompts-JH6YBHHV.js");
7669
8889
  const confirmed = await confirm2(
7670
8890
  "Are you sure you want to rollback?",
7671
8891
  false,
@@ -7787,7 +9007,7 @@ function findApp3(apps, identifier) {
7787
9007
  (app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
7788
9008
  );
7789
9009
  }
7790
- function formatDate5(date) {
9010
+ function formatDate6(date) {
7791
9011
  const d = new Date(date);
7792
9012
  return d.toLocaleString("en-US", {
7793
9013
  month: "short",
@@ -8428,7 +9648,7 @@ function registerDomainsCommands(program2) {
8428
9648
  d.source === "purchased" ? colors.info("purchased") : colors.dim("external"),
8429
9649
  formatStatus(d.status),
8430
9650
  formatCfStatus(d.cloudflareZoneStatus),
8431
- d.source === "purchased" && d.expiryDate ? formatDate6(d.expiryDate) : colors.dim("-"),
9651
+ d.source === "purchased" && d.expiryDate ? formatDate7(d.expiryDate) : colors.dim("-"),
8432
9652
  d.source === "purchased" ? d.autoRenew ? colors.success("on") : colors.warn("off") : colors.dim("-")
8433
9653
  ])
8434
9654
  );
@@ -10348,7 +11568,7 @@ function formatCfStatus(status) {
10348
11568
  return status;
10349
11569
  }
10350
11570
  }
10351
- function formatDate6(dateValue) {
11571
+ function formatDate7(dateValue) {
10352
11572
  try {
10353
11573
  const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
10354
11574
  return date.toISOString().split("T")[0] || "";
@@ -10406,7 +11626,7 @@ function registerEnvCommands(program2) {
10406
11626
  colors.cyan(v.key),
10407
11627
  options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
10408
11628
  v.isSecret ? colors.warn("Yes") : "No",
10409
- formatDate7(v.updatedAt)
11629
+ formatDate8(v.updatedAt)
10410
11630
  ])
10411
11631
  );
10412
11632
  log("");
@@ -10789,7 +12009,7 @@ ${colors.bold(key)}: ${maskValue(val.value || String(v))}
10789
12009
  } else {
10790
12010
  const _raw = await import("process");
10791
12011
  log('Enter JSON key-value object (e.g. {"KEY":"value"}):');
10792
- const input2 = await (await import("./prompts-QQ2FZKQT.js")).input(
12012
+ const input2 = await (await import("./prompts-JH6YBHHV.js")).input(
10793
12013
  "JSON:"
10794
12014
  );
10795
12015
  vars = JSON.parse(input2);
@@ -10812,7 +12032,7 @@ ${colors.bold(key)}: ${maskValue(val.value || String(v))}
10812
12032
  try {
10813
12033
  if (!isLoggedIn()) throw new AuthError();
10814
12034
  if (!shouldSkipConfirmation()) {
10815
- const { confirm: confirmFn } = await import("./prompts-QQ2FZKQT.js");
12035
+ const { confirm: confirmFn } = await import("./prompts-JH6YBHHV.js");
10816
12036
  const ok = await confirmFn(
10817
12037
  `Delete ${keys.length} variable(s)?`,
10818
12038
  false
@@ -10851,7 +12071,7 @@ ${colors.bold(key)}: ${maskValue(val.value || String(v))}
10851
12071
  );
10852
12072
  if (!app) throw new NotFoundError("Application", appIdentifier);
10853
12073
  if (!shouldSkipConfirmation()) {
10854
- const { confirm: confirmFn } = await import("./prompts-QQ2FZKQT.js");
12074
+ const { confirm: confirmFn } = await import("./prompts-JH6YBHHV.js");
10855
12075
  const ok = await confirmFn(
10856
12076
  `Copy env vars from ${fromEnvId} to ${toEnvId}?`,
10857
12077
  false
@@ -10885,7 +12105,7 @@ function maskValue(value) {
10885
12105
  if (value.length <= 4) return "****";
10886
12106
  return `${value.slice(0, 2)}****${value.slice(-2)}`;
10887
12107
  }
10888
- function formatDate7(date) {
12108
+ function formatDate8(date) {
10889
12109
  const d = new Date(date);
10890
12110
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
10891
12111
  }
@@ -11542,7 +12762,7 @@ function registerKeysCommands(program2) {
11542
12762
  k.name,
11543
12763
  colors.dim(`${(k.fingerprint || "").slice(0, 20)}...`),
11544
12764
  k.isDefault ? colors.success("*") : "",
11545
- formatDate8(k.createdAt)
12765
+ formatDate9(k.createdAt)
11546
12766
  ])
11547
12767
  );
11548
12768
  log("");
@@ -11756,7 +12976,7 @@ function findKey(keys, identifier) {
11756
12976
  (k) => (k.keyId || k.id) === identifier || (k.keyId || k.id || "").startsWith(identifier) || k.name.toLowerCase() === lower
11757
12977
  );
11758
12978
  }
11759
- function formatDate8(date) {
12979
+ function formatDate9(date) {
11760
12980
  if (!date) return colors.dim("-");
11761
12981
  return new Date(date).toLocaleDateString("en-US", {
11762
12982
  month: "short",
@@ -12849,7 +14069,7 @@ function registerOrgsCommands(program2) {
12849
14069
  m.user?.name || m.name || colors.dim("-"),
12850
14070
  m.user?.email || m.email || colors.dim("-"),
12851
14071
  formatRole(m.role),
12852
- formatDate9(m.createdAt)
14072
+ formatDate10(m.createdAt)
12853
14073
  ])
12854
14074
  );
12855
14075
  log("");
@@ -12885,7 +14105,7 @@ function registerOrgsCommands(program2) {
12885
14105
  inv.email,
12886
14106
  formatRole(inv.role),
12887
14107
  inv.status || "pending",
12888
- formatDate9(inv.createdAt)
14108
+ formatDate10(inv.createdAt)
12889
14109
  ])
12890
14110
  );
12891
14111
  log("");
@@ -13373,7 +14593,7 @@ function formatRole(role) {
13373
14593
  };
13374
14594
  return map[role] || role;
13375
14595
  }
13376
- function formatDate9(date) {
14596
+ function formatDate10(date) {
13377
14597
  if (!date) return colors.dim("-");
13378
14598
  return new Date(date).toLocaleDateString("en-US", {
13379
14599
  month: "short",
@@ -13659,7 +14879,7 @@ function registerProjectsCommands(program2) {
13659
14879
  }
13660
14880
 
13661
14881
  // src/commands/providers.ts
13662
- import open4 from "open";
14882
+ import open5 from "open";
13663
14883
  function registerProvidersCommands(program2) {
13664
14884
  const providers = program2.command("providers").description("Manage Git providers (GitHub, GitLab, Bitbucket)");
13665
14885
  providers.command("list").alias("ls").description("List all connected Git providers").action(async () => {
@@ -13776,7 +14996,7 @@ function registerProvidersCommands(program2) {
13776
14996
  log(
13777
14997
  "Complete the GitHub browser flow, then connect the repository to your app."
13778
14998
  );
13779
- await open4(url);
14999
+ await open5(url);
13780
15000
  } catch (err) {
13781
15001
  handleError(err);
13782
15002
  }
@@ -14581,7 +15801,7 @@ function registerServersCommands(program2) {
14581
15801
  s.serverSize || s.size || colors.dim("-"),
14582
15802
  getStatusBadge(s.status || "unknown"),
14583
15803
  s.publicIp || s.ip || colors.dim("-"),
14584
- formatDate10(s.createdAt)
15804
+ formatDate11(s.createdAt)
14585
15805
  ])
14586
15806
  );
14587
15807
  log("");
@@ -14743,7 +15963,7 @@ function registerServersCommands(program2) {
14743
15963
  log(` Private IP: ${details.privateIp || colors.dim("-")}`);
14744
15964
  log("");
14745
15965
  if (details.createdAt) {
14746
- log(` Created: ${formatDate10(details.createdAt)}`);
15966
+ log(` Created: ${formatDate11(details.createdAt)}`);
14747
15967
  }
14748
15968
  log("");
14749
15969
  } catch (err) {
@@ -15012,7 +16232,7 @@ function registerServersCommands(program2) {
15012
16232
  s.name || colors.dim("-"),
15013
16233
  s.status || colors.dim("-"),
15014
16234
  formatBytes4(s.diskSizeGb ? s.diskSizeGb * 1024 * 1024 * 1024 : 0),
15015
- formatDate10(s.createdAt)
16235
+ formatDate11(s.createdAt)
15016
16236
  ])
15017
16237
  );
15018
16238
  log("");
@@ -15536,7 +16756,7 @@ function registerServersCommands(program2) {
15536
16756
  colors.cyan(ip.ip || ip.address || "-"),
15537
16757
  ip.region || "-",
15538
16758
  ip.assignedServerId ? colors.dim(ip.assignedServerId.slice(0, 8)) : colors.dim("unassigned"),
15539
- formatDate10(ip.createdAt)
16759
+ formatDate11(ip.createdAt)
15540
16760
  ])
15541
16761
  );
15542
16762
  log("");
@@ -16390,7 +17610,7 @@ function findServer(servers, identifier) {
16390
17610
  (s) => (s.id || s.serverId) === identifier || (s.id || s.serverId || "").startsWith(identifier) || (s.name || "").toLowerCase() === lower
16391
17611
  );
16392
17612
  }
16393
- function formatDate10(date) {
17613
+ function formatDate11(date) {
16394
17614
  if (!date) return colors.dim("-");
16395
17615
  return new Date(date).toLocaleDateString("en-US", {
16396
17616
  month: "short",
@@ -16576,7 +17796,7 @@ function registerStorageCommands(program2) {
16576
17796
  b.plan || colors.dim("free"),
16577
17797
  b.region || colors.dim("-"),
16578
17798
  b.publicAccess ? colors.success("yes") : colors.dim("no"),
16579
- formatDate11(b.createdAt)
17799
+ formatDate12(b.createdAt)
16580
17800
  ])
16581
17801
  );
16582
17802
  log("");
@@ -16759,7 +17979,7 @@ function registerStorageCommands(program2) {
16759
17979
  log(` ${colors.dim("Not available")}`);
16760
17980
  }
16761
17981
  log("");
16762
- log(` Created: ${formatDate11(bucket.createdAt)}`);
17982
+ log(` Created: ${formatDate12(bucket.createdAt)}`);
16763
17983
  log("");
16764
17984
  } catch (err) {
16765
17985
  handleError(err);
@@ -16809,7 +18029,7 @@ function registerStorageCommands(program2) {
16809
18029
  f.name || f.key || "",
16810
18030
  formatBytes5(f.size || f.sizeBytes || 0),
16811
18031
  f.contentType || colors.dim("-"),
16812
- formatDate11(f.updated || f.updatedAt || f.lastModified)
18032
+ formatDate12(f.updated || f.updatedAt || f.lastModified)
16813
18033
  ])
16814
18034
  );
16815
18035
  log("");
@@ -17250,7 +18470,7 @@ function findBucket(buckets, identifier) {
17250
18470
  (b) => (b.bucketId || b.id) === identifier || (b.bucketId || b.id || "").startsWith(identifier) || b.name.toLowerCase() === lower
17251
18471
  );
17252
18472
  }
17253
- function formatDate11(date) {
18473
+ function formatDate12(date) {
17254
18474
  if (!date) return colors.dim("-");
17255
18475
  return new Date(date).toLocaleDateString("en-US", {
17256
18476
  month: "short",
@@ -17312,7 +18532,7 @@ ${colors.warn(`${unread.count} unread ticket${unread.count === 1 ? "" : "s"}`)}`
17312
18532
  formatStatus2(t.status),
17313
18533
  formatPriority(t.priority),
17314
18534
  t.category || colors.dim("-"),
17315
- formatDate12(t.createdAt)
18535
+ formatDate13(t.createdAt)
17316
18536
  ])
17317
18537
  );
17318
18538
  log("");
@@ -17449,7 +18669,7 @@ ${colors.warn(`${unread.count} unread ticket${unread.count === 1 ? "" : "s"}`)}`
17449
18669
  log(` Status: ${formatStatus2(ticket.status)}`);
17450
18670
  log(` Priority: ${formatPriority(ticket.priority)}`);
17451
18671
  log(` Category: ${ticket.category || colors.dim("-")}`);
17452
- log(` Created: ${formatDate12(ticket.createdAt)}`);
18672
+ log(` Created: ${formatDate13(ticket.createdAt)}`);
17453
18673
  log("");
17454
18674
  if (ticket.description) {
17455
18675
  log(colors.bold("Description"));
@@ -17701,7 +18921,7 @@ function formatPriority(priority) {
17701
18921
  };
17702
18922
  return map[priority] || priority;
17703
18923
  }
17704
- function formatDate12(date) {
18924
+ function formatDate13(date) {
17705
18925
  if (!date) return colors.dim("-");
17706
18926
  return new Date(date).toLocaleDateString("en-US", {
17707
18927
  month: "short",
@@ -17921,11 +19141,7 @@ function registerUpCommand(program2) {
17921
19141
  if (isEntitlementError(err)) {
17922
19142
  const message = err instanceof Error ? err.message : "Plan upgrade required";
17923
19143
  if (isJsonMode() || shouldSkipConfirmation()) {
17924
- outputError("NEEDS_UPGRADE", message, {
17925
- suggestedPlan: inferSuggestedPlan(options.plan),
17926
- failedEntitlementKey: extractEntitlementKeyFromError(err),
17927
- hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout up`."
17928
- });
19144
+ await emitNeedsUpgrade(client, err, options.plan, "tarout up");
17929
19145
  exit(ExitCode.PERMISSION_DENIED);
17930
19146
  }
17931
19147
  log("");
@@ -17936,11 +19152,7 @@ function registerUpCommand(program2) {
17936
19152
  options.plan
17937
19153
  );
17938
19154
  if (!upgraded) {
17939
- outputError("NEEDS_UPGRADE", message, {
17940
- suggestedPlan: inferSuggestedPlan(options.plan),
17941
- failedEntitlementKey: extractEntitlementKeyFromError(err),
17942
- hint: "Run `tarout billing upgrade <plan> --wait`, then retry `tarout up`."
17943
- });
19155
+ await emitNeedsUpgrade(client, err, options.plan, "tarout up");
17944
19156
  exit(ExitCode.PERMISSION_DENIED);
17945
19157
  }
17946
19158
  box("Upgrade complete", [
@@ -18149,7 +19361,7 @@ function registerWalletCommands(program2) {
18149
19361
  table(
18150
19362
  ["DATE", "TYPE", "AMOUNT", "BALANCE", "DESCRIPTION"],
18151
19363
  entries.map((e) => [
18152
- formatDate13(e.createdAt),
19364
+ formatDate14(e.createdAt),
18153
19365
  formatType(e.type),
18154
19366
  formatAmount(e.amountHalalas),
18155
19367
  formatAmount(e.balanceAfterHalalas),
@@ -18250,7 +19462,7 @@ function formatType(type) {
18250
19462
  };
18251
19463
  return map[type] || type;
18252
19464
  }
18253
- function formatDate13(iso) {
19465
+ function formatDate14(iso) {
18254
19466
  return new Date(iso).toLocaleDateString("en-US", {
18255
19467
  month: "short",
18256
19468
  day: "numeric"