@tarout/cli 0.3.0 → 0.5.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.5.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;
@@ -3552,43 +3590,1132 @@ function registerBackupsCommands(program2) {
3552
3590
  }
3553
3591
  succeedSpinner("Backup completed!");
3554
3592
  if (isJsonMode()) {
3555
- outputData({ success: true, backupId });
3556
- } else {
3557
- log("");
3558
- log(colors.success("Manual backup completed successfully."));
3559
- log("");
3593
+ outputData({ success: true, backupId });
3594
+ } else {
3595
+ log("");
3596
+ log(colors.success("Manual backup completed successfully."));
3597
+ log("");
3598
+ }
3599
+ } catch (err) {
3600
+ handleError(err);
3601
+ }
3602
+ });
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 items = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
3762
+ result = await client.subscription.purchaseAddons.mutate({ items });
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/lib/plan-cart.ts
3953
+ function planFamily(planKey) {
3954
+ if (!planKey) return null;
3955
+ if (planKey === "free") return "FREE";
3956
+ if (planKey === "shared" || planKey.startsWith("shared_") || planKey.startsWith("bundle_")) {
3957
+ return "SHARED";
3958
+ }
3959
+ if (planKey === "dedicated" || planKey.startsWith("dedicated_")) {
3960
+ return "DEDICATED";
3961
+ }
3962
+ return null;
3963
+ }
3964
+ function isPaidFamily(planKey) {
3965
+ const family = planFamily(planKey);
3966
+ return family === "SHARED" || family === "DEDICATED";
3967
+ }
3968
+ function resourceAddonKeysForPlan(planKey) {
3969
+ switch (planFamily(planKey)) {
3970
+ case "SHARED":
3971
+ return { dbAddonKey: "db.starter", storageAddonKey: "storage.gb" };
3972
+ case "DEDICATED":
3973
+ return { dbAddonKey: "db.pro", storageAddonKey: "storage.gb" };
3974
+ default:
3975
+ return { dbAddonKey: null, storageAddonKey: null };
3976
+ }
3977
+ }
3978
+ function buildPlanAddonCart(planKey, resources) {
3979
+ const { dbAddonKey, storageAddonKey } = resourceAddonKeysForPlan(planKey);
3980
+ const databases = Math.max(0, Math.floor(resources.databases ?? 0));
3981
+ const storageGb = Math.max(0, Math.floor(resources.storageGb ?? 0));
3982
+ const cart = [];
3983
+ if (dbAddonKey && databases > 0) {
3984
+ cart.push({ addonKey: dbAddonKey, quantity: databases });
3985
+ }
3986
+ if (storageAddonKey && storageGb > 0) {
3987
+ cart.push({ addonKey: storageAddonKey, quantity: storageGb });
3988
+ }
3989
+ return cart;
3990
+ }
3991
+
3992
+ // src/commands/billing.ts
3993
+ function reportBillingResult(result, label) {
3994
+ const code = emitBillingResult(result, { label });
3995
+ if (code !== ExitCode.SUCCESS) exit(code);
3996
+ }
3997
+ function browserOpener(noOpen) {
3998
+ if (isJsonMode() || noOpen) return void 0;
3999
+ return async (url) => {
4000
+ await open3(url);
4001
+ };
4002
+ }
4003
+ function isConflictError(err) {
4004
+ const e = err;
4005
+ return (e?.code ?? e?.data?.code) === "CONFLICT";
4006
+ }
4007
+ function registerBillingCommands(program2) {
4008
+ const billing = program2.command("billing").description("Manage subscription and billing");
4009
+ billing.command("status").description("Show current subscription and entitlements").action(async () => {
4010
+ try {
4011
+ if (!isLoggedIn()) throw new AuthError();
4012
+ const client = getApiClient();
4013
+ const _spinner = startSpinner("Fetching subscription...");
4014
+ const subscription = await client.subscription.getCurrent.query();
4015
+ succeedSpinner();
4016
+ if (isJsonMode()) {
4017
+ outputData(subscription);
4018
+ return;
4019
+ }
4020
+ log("");
4021
+ log(colors.bold("Current Subscription"));
4022
+ log("");
4023
+ if (!subscription || !subscription.planKey) {
4024
+ log(` Plan: ${colors.dim("No active subscription (free tier)")}`);
4025
+ } else {
4026
+ log(` Plan: ${colors.cyan(subscription.planKey)}`);
4027
+ if (subscription.planQuantity && subscription.planQuantity > 1) {
4028
+ log(` Quantity: ${subscription.planQuantity}`);
4029
+ }
4030
+ log(` Status: ${formatSubStatus(subscription.status || "active")}`);
4031
+ if (subscription.currentPeriodEnd) {
4032
+ log(` Renews: ${formatDate4(subscription.currentPeriodEnd)}`);
4033
+ }
4034
+ if (subscription.cancelAtPeriodEnd) {
4035
+ log(` ${colors.warn("\u26A0 Cancels at end of billing period")}`);
4036
+ }
4037
+ }
4038
+ if (subscription?.items && subscription.items.length > 0) {
4039
+ log("");
4040
+ log(colors.bold("Add-ons"));
4041
+ table(
4042
+ ["ADDON", "QUANTITY"],
4043
+ subscription.items.map((item) => [
4044
+ colors.cyan(item.addonKey || item.key || ""),
4045
+ String(item.quantity || 1)
4046
+ ])
4047
+ );
4048
+ }
4049
+ log("");
4050
+ log(`To view available plans: ${colors.dim("tarout billing plans")}`);
4051
+ } catch (err) {
4052
+ handleError(err);
4053
+ }
4054
+ });
4055
+ billing.command("plans").description("List available subscription plans").action(async () => {
4056
+ try {
4057
+ if (!isLoggedIn()) throw new AuthError();
4058
+ const client = getApiClient();
4059
+ const _spinner = startSpinner("Fetching plans...");
4060
+ const catalog = await client.subscription.getCatalog.query();
4061
+ succeedSpinner();
4062
+ if (isJsonMode()) {
4063
+ outputData(catalog);
4064
+ return;
4065
+ }
4066
+ log("");
4067
+ log(colors.bold("Available Plans"));
4068
+ log("");
4069
+ const plans = catalog?.plans || catalog || [];
4070
+ if (!Array.isArray(plans) || plans.length === 0) {
4071
+ log("No plans available.");
4072
+ return;
4073
+ }
4074
+ table(
4075
+ ["PLAN", "PRICE", "DESCRIPTION"],
4076
+ plans.map((p) => [
4077
+ colors.cyan(p.planKey || p.key || p.name || ""),
4078
+ p.priceHalalas ? `${(p.priceHalalas / 100).toFixed(2)} SAR/mo` : colors.dim("Free"),
4079
+ p.description || ""
4080
+ ])
4081
+ );
4082
+ log("");
4083
+ log(`To upgrade: ${colors.dim("tarout billing upgrade <plan>")}`);
4084
+ } catch (err) {
4085
+ handleError(err);
4086
+ }
4087
+ });
4088
+ billing.command("upgrade").argument("[plan]", "Plan key to switch to (alias: --plan)").description("Upgrade or change subscription plan").option(
4089
+ "--plan <key>",
4090
+ "Plan key (alias for the positional argument; useful for agent invocations)"
4091
+ ).option(
4092
+ "-q, --quantity <n>",
4093
+ "Plan quantity (for multi-slot plans)",
4094
+ Number.parseInt
4095
+ ).option(
4096
+ "--billing-period <period>",
4097
+ "Billing period: monthly or yearly (yearly = 10\xD7 monthly, 2 months free)",
4098
+ parseBillingPeriod
4099
+ ).option(
4100
+ "--addon <key[:qty]>",
4101
+ "Bundled addon to purchase with the plan change (repeatable, e.g. --addon db.standard:2)",
4102
+ collectAddon,
4103
+ []
4104
+ ).option(
4105
+ "-w, --wait",
4106
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4107
+ ).option(
4108
+ "--timeout <seconds>",
4109
+ "Maximum wait time in seconds (default 600)",
4110
+ (v) => Number.parseInt(v, 10),
4111
+ 600
4112
+ ).option(
4113
+ "--no-open",
4114
+ "Do not auto-open the payment URL in the default browser"
4115
+ ).action(async (planKey, options) => {
4116
+ try {
4117
+ if (!isLoggedIn()) throw new AuthError();
4118
+ const client = getApiClient();
4119
+ let targetPlan = planKey || options.plan;
4120
+ const billingPeriod = options.billingPeriod;
4121
+ let planQuantity = options.quantity;
4122
+ let addons = Array.isArray(options.addon) && options.addon.length > 0 ? options.addon : void 0;
4123
+ if (!targetPlan) {
4124
+ const _spinner = startSpinner("Fetching plans...");
4125
+ const catalog = await client.subscription.getCatalog.query();
4126
+ succeedSpinner();
4127
+ const plans = catalog?.plans || catalog || [];
4128
+ if (!Array.isArray(plans) || plans.length === 0) {
4129
+ log("No plans available.");
4130
+ return;
4131
+ }
4132
+ targetPlan = await select(
4133
+ "Select a plan:",
4134
+ plans.map((p) => ({
4135
+ name: `${p.planKey || p.key || p.name} ${p.priceHalalas ? `(${(p.priceHalalas / 100).toFixed(2)} SAR/mo)` : "(Free)"}`,
4136
+ value: p.planKey || p.key || p.name
4137
+ })),
4138
+ {
4139
+ field: "plan",
4140
+ flag: "--plan",
4141
+ context: {
4142
+ available: plans.map((p) => ({
4143
+ key: p.planKey || p.key || p.name,
4144
+ priceHalalas: p.priceHalalas ?? 0
4145
+ }))
4146
+ }
4147
+ }
4148
+ );
4149
+ }
4150
+ if (!targetPlan) {
4151
+ throw new Error("No plan selected");
4152
+ }
4153
+ if (!isJsonMode() && !isNonInteractiveMode() && !shouldSkipConfirmation() && !addons && isPaidFamily(targetPlan)) {
4154
+ if (planQuantity === void 0 && planFamily(targetPlan) === "SHARED") {
4155
+ const apps = parsePositiveInt(
4156
+ await input("How many apps (app slots)?", "1"),
4157
+ 1
4158
+ );
4159
+ planQuantity = Math.max(1, apps);
4160
+ }
4161
+ const databases = parsePositiveInt(
4162
+ await input("How many databases to include?", "1"),
4163
+ 0
4164
+ );
4165
+ const storageGb = parsePositiveInt(
4166
+ await input("Object storage to include (GB, 0 for none)?", "5"),
4167
+ 0
4168
+ );
4169
+ const cart = buildPlanAddonCart(targetPlan, { databases, storageGb });
4170
+ if (cart.length > 0) addons = cart;
4171
+ }
4172
+ const _previewSpinner = startSpinner("Calculating change...");
4173
+ let preview;
4174
+ try {
4175
+ preview = await client.subscription.previewPlanChange.query({
4176
+ planKey: targetPlan,
4177
+ planQuantity,
4178
+ billingPeriod,
4179
+ addons
4180
+ });
4181
+ succeedSpinner();
4182
+ } catch {
4183
+ failSpinner();
4184
+ preview = null;
4185
+ }
4186
+ if (!shouldSkipConfirmation()) {
4187
+ log("");
4188
+ log(`Plan: ${colors.cyan(targetPlan)}`);
4189
+ if (planQuantity) log(`Quantity: ${planQuantity}`);
4190
+ if (billingPeriod) log(`Billing period: ${billingPeriod}`);
4191
+ if (addons && addons.length > 0) {
4192
+ log(
4193
+ `Addons: ${addons.map((a) => `${a.addonKey}\xD7${a.quantity}`).join(", ")}`
4194
+ );
4195
+ }
4196
+ const amountDueHalalas = typeof preview?.proratedChargeHalalas === "number" ? preview.proratedChargeHalalas : void 0;
4197
+ if (amountDueHalalas !== void 0) {
4198
+ log(
4199
+ `Amount due now: ${colors.bold(`${(amountDueHalalas / 100).toFixed(2)} SAR`)} ${colors.dim("(incl. 15% VAT for SA orgs at checkout)")}`
4200
+ );
4201
+ }
4202
+ if (typeof preview?.newPeriodTotalHalalas === "number") {
4203
+ log(
4204
+ `New recurring total: ${(preview.newPeriodTotalHalalas / 100).toFixed(2)} SAR`
4205
+ );
4206
+ }
4207
+ log("");
4208
+ const confirmed = await confirm(
4209
+ `Switch to plan "${targetPlan}"?`,
4210
+ false,
4211
+ {
4212
+ field: "confirm_upgrade",
4213
+ flag: "--yes",
4214
+ context: {
4215
+ plan: targetPlan,
4216
+ quantity: planQuantity,
4217
+ billingPeriod,
4218
+ addons,
4219
+ amountDueHalalas
4220
+ }
4221
+ }
4222
+ );
4223
+ if (!confirmed) {
4224
+ log("Cancelled.");
4225
+ return;
4226
+ }
4227
+ }
4228
+ const _changeSpinner = startSpinner("Changing plan...");
4229
+ const result = await performBillingChange(client, {
4230
+ kind: "plan",
4231
+ planKey: targetPlan,
4232
+ quantity: planQuantity,
4233
+ billingPeriod,
4234
+ addons,
4235
+ wait: options.wait,
4236
+ timeoutMs: options.timeout * 1e3,
4237
+ openBrowser: browserOpener(options.open === false),
4238
+ onCheckoutOpened: ({ orderId, paymentUrl }) => {
4239
+ if (isJsonMode()) {
4240
+ outputJsonLine({
4241
+ type: "event",
4242
+ event: "checkout_started",
4243
+ orderId,
4244
+ paymentUrl
4245
+ });
4246
+ } else {
4247
+ log("");
4248
+ log("Open this URL to complete payment:");
4249
+ log(` ${colors.cyan(paymentUrl)}`);
4250
+ log(`Order ID: ${colors.dim(orderId)}`);
4251
+ log(`Polling for confirmation (up to ${options.timeout}s)...`);
4252
+ }
4253
+ }
4254
+ });
4255
+ succeedSpinner("Plan change processed.");
4256
+ reportBillingResult(result, `Plan: ${targetPlan}`);
4257
+ } catch (err) {
4258
+ handleError(err);
4259
+ }
4260
+ });
4261
+ billing.command("confirm").argument("<orderId>", "Order ID returned by `billing upgrade`").description("Manually confirm a pending checkout (skips browser flow)").action(async (orderId) => {
4262
+ try {
4263
+ if (!isLoggedIn()) throw new AuthError();
4264
+ const client = getApiClient();
4265
+ const _s = startSpinner("Confirming checkout...");
4266
+ const result = await client.subscription.confirmCheckout.mutate({
4267
+ orderId
4268
+ });
4269
+ succeedSpinner("Checkout confirmed.");
4270
+ const mapped = {
4271
+ status: result?.applied ? "paid" : "payment_required",
4272
+ kind: "plan",
4273
+ target: orderId,
4274
+ orderId,
4275
+ ...result?.paymentUrl ? { paymentUrl: result.paymentUrl } : {}
4276
+ };
4277
+ reportBillingResult(mapped, `Order ${orderId.slice(0, 8)}`);
4278
+ } catch (err) {
4279
+ handleError(err);
4280
+ }
4281
+ });
4282
+ billing.command("wait").argument("<orderId>", "Order ID to wait on").description("Poll a pending checkout until it resolves").option(
4283
+ "--timeout <seconds>",
4284
+ "Maximum wait time in seconds (default 600)",
4285
+ (v) => Number.parseInt(v, 10),
4286
+ 600
4287
+ ).action(async (orderId, options) => {
4288
+ try {
4289
+ if (!isLoggedIn()) throw new AuthError();
4290
+ const client = getApiClient();
4291
+ if (isJsonMode()) {
4292
+ outputJsonLine({
4293
+ type: "event",
4294
+ event: "checkout_polling_started",
4295
+ orderId,
4296
+ timeoutSeconds: options.timeout
4297
+ });
4298
+ }
4299
+ const final = await pollCheckoutUntilTerminal(client, orderId, {
4300
+ timeoutMs: options.timeout * 1e3,
4301
+ intervalMs: 4e3
4302
+ });
4303
+ const result = {
4304
+ status: final.status === "PAID" ? "paid" : final.status === "FAILED" ? "failed" : final.status === "EXPIRED" ? "expired" : "pending_timeout",
4305
+ kind: "plan",
4306
+ target: orderId,
4307
+ orderId,
4308
+ failureReason: final.failureReason
4309
+ };
4310
+ reportBillingResult(result, `Order ${orderId.slice(0, 8)}`);
4311
+ } catch (err) {
4312
+ handleError(err);
4313
+ }
4314
+ });
4315
+ billing.command("cancel").description("Cancel current subscription (at period end)").action(async () => {
4316
+ try {
4317
+ if (!isLoggedIn()) throw new AuthError();
4318
+ if (!shouldSkipConfirmation()) {
4319
+ log("");
4320
+ log(
4321
+ colors.warn(
4322
+ "Cancelling your subscription will downgrade to free tier at the end of the billing period."
4323
+ )
4324
+ );
4325
+ log("");
4326
+ const confirmed = await confirm(
4327
+ "Are you sure you want to cancel your subscription?",
4328
+ false,
4329
+ { field: "confirm_cancel", flag: "--yes" }
4330
+ );
4331
+ if (!confirmed) {
4332
+ log("Cancelled.");
4333
+ return;
4334
+ }
4335
+ }
4336
+ const client = getApiClient();
4337
+ const _spinner = startSpinner("Cancelling subscription...");
4338
+ await client.subscription.cancel.mutate();
4339
+ succeedSpinner("Subscription scheduled for cancellation");
4340
+ if (isJsonMode()) {
4341
+ outputData({ cancelled: true });
4342
+ } else {
4343
+ log("");
4344
+ log(
4345
+ "Your subscription will remain active until the end of the current billing period."
4346
+ );
4347
+ log(`To undo: ${colors.dim("tarout billing resume")}`);
4348
+ log("");
4349
+ }
4350
+ } catch (err) {
4351
+ handleError(err);
4352
+ }
4353
+ });
4354
+ billing.command("resume").description("Resume a cancelled subscription").action(async () => {
4355
+ try {
4356
+ if (!isLoggedIn()) throw new AuthError();
4357
+ const client = getApiClient();
4358
+ const _spinner = startSpinner("Resuming subscription...");
4359
+ await client.subscription.resume.mutate();
4360
+ succeedSpinner("Subscription resumed!");
4361
+ if (isJsonMode()) {
4362
+ outputData({ resumed: true });
4363
+ } else {
4364
+ log("");
4365
+ log(
4366
+ colors.success(
4367
+ "Your subscription has been resumed and will continue normally."
4368
+ )
4369
+ );
4370
+ log("");
4371
+ }
4372
+ } catch (err) {
4373
+ handleError(err);
4374
+ }
4375
+ });
4376
+ billing.command("addon:add").argument("<addon>", "Addon key to add").option("-q, --quantity <n>", "Addon quantity", Number.parseInt).option(
4377
+ "-w, --wait",
4378
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4379
+ ).option(
4380
+ "--timeout <seconds>",
4381
+ "Maximum wait time in seconds (default 600)",
4382
+ (v) => Number.parseInt(v, 10),
4383
+ 600
4384
+ ).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) => {
4385
+ try {
4386
+ if (!isLoggedIn()) throw new AuthError();
4387
+ const quantity = options.quantity || 1;
4388
+ if (!shouldSkipConfirmation()) {
4389
+ log("");
4390
+ log(`Addon: ${colors.cyan(addonKey)}`);
4391
+ log(`Quantity: ${quantity}`);
4392
+ log("");
4393
+ const confirmed = await confirm(
4394
+ `Add addon "${addonKey}" \xD7 ${quantity}?`,
4395
+ false,
4396
+ {
4397
+ field: "confirm_addon_add",
4398
+ flag: "--yes",
4399
+ context: { addonKey, quantity }
4400
+ }
4401
+ );
4402
+ if (!confirmed) {
4403
+ log("Cancelled.");
4404
+ return;
4405
+ }
4406
+ }
4407
+ const client = getApiClient();
4408
+ const _spinner = startSpinner("Adding addon...");
4409
+ let raw;
4410
+ try {
4411
+ raw = await client.subscription.addAddon.mutate({
4412
+ addonKey,
4413
+ quantity
4414
+ });
4415
+ } catch (err) {
4416
+ failSpinner();
4417
+ if (isConflictError(err)) {
4418
+ const nextCommand = `tarout billing addon:quantity ${addonKey} <newQty>`;
4419
+ outputError(
4420
+ "ADDON_EXISTS",
4421
+ `Addon "${addonKey}" is already on your subscription \u2014 change its quantity instead of adding it again.`,
4422
+ { addonKey, nextCommand }
4423
+ );
4424
+ if (!isJsonMode()) {
4425
+ box("Addon already present", [
4426
+ `Use: ${colors.dim(nextCommand)}`,
4427
+ `Or buy more slots: ${colors.dim(`tarout billing addon:buy ${addonKey} --wait`)}`
4428
+ ]);
4429
+ }
4430
+ exit(ExitCode.INVALID_ARGUMENTS);
4431
+ return;
4432
+ }
4433
+ throw err;
4434
+ }
4435
+ succeedSpinner("Addon processed.");
4436
+ const result = await finalizeBillingMutation(client, raw, {
4437
+ kind: "addon",
4438
+ target: addonKey,
4439
+ wait: options.wait,
4440
+ timeoutMs: options.timeout * 1e3,
4441
+ openBrowser: browserOpener(options.open === false)
4442
+ });
4443
+ reportBillingResult(result, `Addon: ${addonKey} \xD7${quantity}`);
4444
+ } catch (err) {
4445
+ handleError(err);
4446
+ }
4447
+ });
4448
+ billing.command("addon:remove").argument("<addon>", "Addon key to remove").description("Remove an addon").action(async (addonKey) => {
4449
+ try {
4450
+ if (!isLoggedIn()) throw new AuthError();
4451
+ if (!shouldSkipConfirmation()) {
4452
+ const confirmed = await confirm(
4453
+ `Remove addon "${addonKey}"?`,
4454
+ false,
4455
+ {
4456
+ field: "confirm_addon_remove",
4457
+ flag: "--yes",
4458
+ context: { addonKey }
4459
+ }
4460
+ );
4461
+ if (!confirmed) {
4462
+ log("Cancelled.");
4463
+ return;
4464
+ }
4465
+ }
4466
+ const client = getApiClient();
4467
+ const _spinner = startSpinner("Removing addon...");
4468
+ await client.subscription.removeAddon.mutate({ addonKey });
4469
+ succeedSpinner("Addon removed!");
4470
+ if (isJsonMode()) {
4471
+ outputData({ removed: true, addonKey });
4472
+ }
4473
+ } catch (err) {
4474
+ handleError(err);
4475
+ }
4476
+ });
4477
+ billing.command("plan:quantity").argument("<quantity>", "New plan quantity", Number.parseInt).option(
4478
+ "-w, --wait",
4479
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4480
+ ).option(
4481
+ "--timeout <seconds>",
4482
+ "Maximum wait time in seconds (default 600)",
4483
+ (v) => Number.parseInt(v, 10),
4484
+ 600
4485
+ ).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) => {
4486
+ try {
4487
+ if (!isLoggedIn()) throw new AuthError();
4488
+ const client = getApiClient();
4489
+ const _spinner = startSpinner("Updating plan quantity...");
4490
+ const result = await performBillingChange(client, {
4491
+ kind: "plan_quantity",
4492
+ quantity,
4493
+ wait: options.wait,
4494
+ timeoutMs: options.timeout * 1e3,
4495
+ openBrowser: browserOpener(options.open === false)
4496
+ });
4497
+ succeedSpinner("Plan quantity processed.");
4498
+ reportBillingResult(result, `Plan quantity: ${quantity}`);
4499
+ } catch (err) {
4500
+ handleError(err);
4501
+ }
4502
+ });
4503
+ 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) => {
4504
+ try {
4505
+ if (!isLoggedIn()) throw new AuthError();
4506
+ const client = getApiClient();
4507
+ const _spinner = startSpinner("Updating addon quantity...");
4508
+ const result = await client.subscription.updateAddonQuantity.mutate({
4509
+ addonKey,
4510
+ quantity
4511
+ });
4512
+ succeedSpinner("Addon quantity updated!");
4513
+ if (isJsonMode()) outputData(result);
4514
+ } catch (err) {
4515
+ handleError(err);
4516
+ }
4517
+ });
4518
+ 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) => {
4519
+ try {
4520
+ if (!isLoggedIn()) throw new AuthError();
4521
+ const client = getApiClient();
4522
+ const _spinner = startSpinner("Calculating preview...");
4523
+ const preview = await client.subscription.previewAddonsPurchase.query({
4524
+ items: [{ addonKey, quantity: options.quantity || 1 }]
4525
+ });
4526
+ succeedSpinner();
4527
+ if (isJsonMode()) {
4528
+ outputData(preview);
4529
+ return;
4530
+ }
4531
+ const p = preview;
4532
+ log("");
4533
+ log(colors.bold("Addon Purchase Preview"));
4534
+ log(` Addon: ${colors.cyan(addonKey)}`);
4535
+ log(` Quantity: ${options.quantity || 1}`);
4536
+ if (typeof p?.totalProratedHalalas === "number") {
4537
+ log(
4538
+ ` Amount Due: ${colors.bold(`${(p.totalProratedHalalas / 100).toFixed(2)} SAR`)} ${colors.dim("(incl. 15% VAT for SA orgs at checkout)")}`
4539
+ );
4540
+ }
4541
+ if (typeof p?.newPeriodTotalHalalas === "number") {
4542
+ log(
4543
+ ` New total: ${(p.newPeriodTotalHalalas / 100).toFixed(2)} SAR`
4544
+ );
4545
+ }
4546
+ log("");
4547
+ } catch (err) {
4548
+ handleError(err);
4549
+ }
4550
+ });
4551
+ billing.command("addon:buy").argument("<addon>", "Addon key").option("-q, --quantity <n>", "Quantity", Number.parseInt).option(
4552
+ "-w, --wait",
4553
+ "After hosted-checkout opens, poll status until the payment is confirmed"
4554
+ ).option(
4555
+ "--timeout <seconds>",
4556
+ "Maximum wait time in seconds (default 600)",
4557
+ (v) => Number.parseInt(v, 10),
4558
+ 600
4559
+ ).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) => {
4560
+ try {
4561
+ if (!isLoggedIn()) throw new AuthError();
4562
+ const quantity = options.quantity || 1;
4563
+ if (!shouldSkipConfirmation()) {
4564
+ log(`
4565
+ Purchase ${quantity}\xD7 ${colors.cyan(addonKey)}?`);
4566
+ const confirmed = await confirm("Proceed?", false, {
4567
+ field: "confirm_addon_buy",
4568
+ flag: "--yes",
4569
+ context: { addonKey, quantity }
4570
+ });
4571
+ if (!confirmed) {
4572
+ log("Cancelled.");
4573
+ return;
4574
+ }
4575
+ }
4576
+ const client = getApiClient();
4577
+ const _spinner = startSpinner("Purchasing addon...");
4578
+ const result = await performBillingChange(client, {
4579
+ kind: "addon",
4580
+ addonKey,
4581
+ quantity,
4582
+ wait: options.wait,
4583
+ timeoutMs: options.timeout * 1e3,
4584
+ openBrowser: browserOpener(options.open === false)
4585
+ });
4586
+ succeedSpinner("Addon purchase processed.");
4587
+ reportBillingResult(result, `Addon: ${addonKey} \xD7${quantity}`);
4588
+ } catch (err) {
4589
+ handleError(err);
4590
+ }
4591
+ });
4592
+ billing.command("plan:cancel-pending").description("Cancel a pending plan change (keeps current plan)").action(async () => {
4593
+ try {
4594
+ if (!isLoggedIn()) throw new AuthError();
4595
+ if (!shouldSkipConfirmation()) {
4596
+ const confirmed = await confirm(
4597
+ "Cancel the pending plan change?",
4598
+ false,
4599
+ { field: "confirm_pending_cancel", flag: "--yes" }
4600
+ );
4601
+ if (!confirmed) {
4602
+ log("Cancelled.");
4603
+ return;
4604
+ }
4605
+ }
4606
+ const client = getApiClient();
4607
+ const _spinner = startSpinner("Cancelling pending change...");
4608
+ await client.subscription.cancelPendingPlanChange.mutate();
4609
+ succeedSpinner("Pending plan change cancelled!");
4610
+ if (isJsonMode()) outputData({ cancelled: true });
4611
+ } catch (err) {
4612
+ handleError(err);
4613
+ }
4614
+ });
4615
+ billing.command("checkout:confirm").argument("<session-id>", "Checkout session ID").description("Confirm a pending checkout session").action(async (sessionId) => {
4616
+ try {
4617
+ if (!isLoggedIn()) throw new AuthError();
4618
+ const client = getApiClient();
4619
+ const _spinner = startSpinner("Confirming checkout...");
4620
+ const result = await client.subscription.confirmCheckout.mutate({
4621
+ sessionId
4622
+ });
4623
+ succeedSpinner("Checkout confirmed!");
4624
+ if (isJsonMode()) outputData(result);
4625
+ } catch (err) {
4626
+ handleError(err);
4627
+ }
4628
+ });
4629
+ billing.command("checkout:get").argument("<session-id>", "Checkout session ID").description("Get details of a checkout session").action(async (sessionId) => {
4630
+ try {
4631
+ if (!isLoggedIn()) throw new AuthError();
4632
+ const client = getApiClient();
4633
+ const _spinner = startSpinner("Fetching checkout...");
4634
+ const data = await client.payment.getCheckout.query({
4635
+ sessionId
4636
+ });
4637
+ succeedSpinner();
4638
+ if (isJsonMode()) outputData(data);
4639
+ else {
4640
+ const d = data;
4641
+ log("");
4642
+ log(colors.bold("Checkout Session"));
4643
+ log(` ID: ${d.sessionId || sessionId}`);
4644
+ log(` Status: ${d.status || "-"}`);
4645
+ log(
4646
+ ` Amount: ${d.amount !== void 0 ? `${(d.amount / 100).toFixed(2)} SAR` : "-"}`
4647
+ );
4648
+ log("");
4649
+ }
4650
+ } catch (err) {
4651
+ handleError(err);
4652
+ }
4653
+ });
4654
+ 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) => {
4655
+ try {
4656
+ if (!isLoggedIn()) throw new AuthError();
4657
+ const client = getApiClient();
4658
+ const _spinner = startSpinner("Processing payment...");
4659
+ const result = await client.payment.submitCardPayment.mutate({
4660
+ sessionId,
4661
+ cardNumber: options.cardNumber,
4662
+ expMonth: options.expMonth,
4663
+ expYear: options.expYear,
4664
+ cvv: options.cvv,
4665
+ cardholderName: options.name
4666
+ });
4667
+ succeedSpinner("Payment submitted!");
4668
+ if (isJsonMode()) outputData(result);
4669
+ } catch (err) {
4670
+ handleError(err);
4671
+ }
4672
+ });
4673
+ billing.command("usage").description("Show resource usage breakdown").action(async () => {
4674
+ try {
4675
+ if (!isLoggedIn()) throw new AuthError();
4676
+ const client = getApiClient();
4677
+ const _spinner = startSpinner("Fetching usage...");
4678
+ const data = await client.billing.getUsageBreakdown.query();
4679
+ succeedSpinner();
4680
+ if (isJsonMode()) {
4681
+ outputData(data);
4682
+ return;
3560
4683
  }
4684
+ const d = data;
4685
+ log("");
4686
+ log(colors.bold("Usage Breakdown"));
4687
+ log(` Apps: ${d.apps ?? "-"}`);
4688
+ log(` Databases: ${d.databases ?? "-"}`);
4689
+ log(` Storage: ${d.storage ?? "-"}`);
4690
+ log(` Domains: ${d.domains ?? "-"}`);
4691
+ log("");
3561
4692
  } catch (err) {
3562
4693
  handleError(err);
3563
4694
  }
3564
4695
  });
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) => {
4696
+ billing.command("resources").description("Show detailed resource usage and costs").action(async () => {
3566
4697
  try {
3567
4698
  if (!isLoggedIn()) throw new AuthError();
3568
4699
  const client = getApiClient();
3569
- const _spinner = startSpinner("Listing backup files...");
3570
- const files = await client.backup.listBackupFiles.query({
3571
- destinationId,
3572
- search: options.search || ""
3573
- });
4700
+ const _spinner = startSpinner("Fetching resource usage...");
4701
+ const data = await client.billing.getDetailedResourceUsage.query();
3574
4702
  succeedSpinner();
3575
4703
  if (isJsonMode()) {
3576
- outputData(files);
4704
+ outputData(data);
3577
4705
  return;
3578
4706
  }
3579
- const list = Array.isArray(files) ? files : [];
4707
+ const list = Array.isArray(data) ? data : data?.resources || [];
3580
4708
  if (!list.length) {
3581
- log("");
3582
- log("No backup files found.");
4709
+ log("\nNo active resources.\n");
3583
4710
  return;
3584
4711
  }
3585
4712
  log("");
3586
4713
  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"
4714
+ ["TYPE", "NAME", "COST (SAR)"],
4715
+ list.map((r) => [
4716
+ r.type || "-",
4717
+ r.name || "-",
4718
+ r.cost !== void 0 ? (r.cost / 100).toFixed(2) : "-"
3592
4719
  ])
3593
4720
  );
3594
4721
  log("");
@@ -3596,85 +4723,136 @@ function registerBackupsCommands(program2) {
3596
4723
  handleError(err);
3597
4724
  }
3598
4725
  });
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) => {
4726
+ billing.command("trend").description("Show daily cost trend").option("--days <n>", "Number of days", "30").action(async (options) => {
3600
4727
  try {
3601
4728
  if (!isLoggedIn()) throw new AuthError();
3602
4729
  const client = getApiClient();
3603
- const _spinner = startSpinner("Generating download URL...");
3604
- const result = await client.backup.getBackupDownloadUrl.mutate({
3605
- destinationId,
3606
- backupFile
4730
+ const _spinner = startSpinner("Fetching cost trend...");
4731
+ const data = await client.billing.getDailyCostTrend.query({
4732
+ days: Number.parseInt(options.days || "30")
3607
4733
  });
3608
4734
  succeedSpinner();
3609
4735
  if (isJsonMode()) {
3610
- outputData(result);
4736
+ outputData(data);
4737
+ return;
4738
+ }
4739
+ const list = Array.isArray(data) ? data : data?.trend || [];
4740
+ if (!list.length) {
4741
+ log("\nNo cost data found.\n");
3611
4742
  return;
3612
4743
  }
3613
4744
  log("");
3614
- log(`Download URL for ${colors.cyan(backupFile)}:`);
3615
- log("");
3616
- log(` ${colors.cyan(result.url || String(result))}`);
4745
+ log(colors.bold("Daily Cost Trend"));
4746
+ table(
4747
+ ["DATE", "COST (SAR)"],
4748
+ list.map((d) => [
4749
+ d.date || "-",
4750
+ d.cost !== void 0 ? (d.cost / 100).toFixed(2) : "-"
4751
+ ])
4752
+ );
3617
4753
  log("");
3618
4754
  } catch (err) {
3619
4755
  handleError(err);
3620
4756
  }
3621
4757
  });
3622
- backups.command("backup-web").argument("<backup-id>", "Backup configuration ID").description("Manually trigger a web server / app backup").action(async (backupId) => {
4758
+ 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
4759
  try {
3624
4760
  if (!isLoggedIn()) throw new AuthError();
3625
4761
  const client = getApiClient();
3626
- const _spinner = startSpinner("Triggering web server backup...");
3627
- const result = await client.backup.manualBackupWebServer.mutate({
3628
- backupId
4762
+ const _spinner = startSpinner("Calculating estimate...");
4763
+ const data = await client.billing.getResourceEstimate.query({
4764
+ resourceType: options.type,
4765
+ planKey: options.plan
3629
4766
  });
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("");
4767
+ succeedSpinner();
4768
+ if (isJsonMode()) {
4769
+ outputData(data);
4770
+ return;
3636
4771
  }
4772
+ const d = data;
4773
+ log("");
4774
+ log(colors.bold("Resource Estimate"));
4775
+ log(
4776
+ ` Monthly: ${d.monthly !== void 0 ? `${(d.monthly / 100).toFixed(2)} SAR` : "-"}`
4777
+ );
4778
+ log(
4779
+ ` Hourly: ${d.hourly !== void 0 ? `${(d.hourly / 100).toFixed(4)} SAR` : "-"}`
4780
+ );
4781
+ log("");
3637
4782
  } catch (err) {
3638
4783
  handleError(err);
3639
4784
  }
3640
4785
  });
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) => {
4786
+ billing.command("active-resources").description("List all active billable resources").action(async () => {
3642
4787
  try {
3643
4788
  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
4789
  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("");
4790
+ const _spinner = startSpinner("Fetching active resources...");
4791
+ const data = await client.billing.getActiveResources.query();
4792
+ succeedSpinner();
4793
+ if (isJsonMode()) {
4794
+ outputData(data);
4795
+ return;
3671
4796
  }
4797
+ const list = Array.isArray(data) ? data : data?.resources || [];
4798
+ if (!list.length) {
4799
+ log("\nNo active billable resources.\n");
4800
+ return;
4801
+ }
4802
+ log("");
4803
+ table(
4804
+ ["TYPE", "NAME", "STATUS", "PLAN"],
4805
+ list.map((r) => [
4806
+ r.type || "-",
4807
+ r.name || "-",
4808
+ r.status || "-",
4809
+ r.plan || r.planKey || "-"
4810
+ ])
4811
+ );
4812
+ log("");
3672
4813
  } catch (err) {
3673
4814
  handleError(err);
3674
4815
  }
3675
4816
  });
3676
4817
  }
3677
- function formatDate3(date) {
4818
+ function collectAddon(value, previous) {
4819
+ const [rawKey, rawQty] = value.split(":");
4820
+ const addonKey = (rawKey || "").trim();
4821
+ if (!addonKey) {
4822
+ throw new InvalidArgumentError2(
4823
+ `Invalid --addon value "${value}". Expected key[:qty] (e.g. db.standard:2).`
4824
+ );
4825
+ }
4826
+ const quantity = rawQty === void 0 ? 1 : Number.parseInt(rawQty, 10);
4827
+ if (!Number.isFinite(quantity) || quantity <= 0) {
4828
+ throw new InvalidArgumentError2(
4829
+ `Invalid --addon quantity in "${value}". Expected a positive integer.`
4830
+ );
4831
+ }
4832
+ return [...previous, { addonKey, quantity }];
4833
+ }
4834
+ function parsePositiveInt(raw, fallback) {
4835
+ const n = Number.parseInt(String(raw).trim(), 10);
4836
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
4837
+ }
4838
+ function parseBillingPeriod(raw) {
4839
+ const v = raw.toLowerCase();
4840
+ if (v === "monthly" || v === "yearly") return v;
4841
+ throw new InvalidArgumentError2(
4842
+ `Invalid --billing-period "${raw}". Expected "monthly" or "yearly".`
4843
+ );
4844
+ }
4845
+ function formatSubStatus(status) {
4846
+ const map = {
4847
+ active: colors.success("active"),
4848
+ trialing: colors.info("trialing"),
4849
+ past_due: colors.warn("past due"),
4850
+ cancelled: colors.error("cancelled"),
4851
+ none: colors.dim("none")
4852
+ };
4853
+ return map[status] || status;
4854
+ }
4855
+ function formatDate4(date) {
3678
4856
  if (!date) return colors.dim("-");
3679
4857
  return new Date(date).toLocaleDateString("en-US", {
3680
4858
  month: "short",
@@ -3682,12 +4860,6 @@ function formatDate3(date) {
3682
4860
  year: "numeric"
3683
4861
  });
3684
4862
  }
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
4863
 
3692
4864
  // src/lib/process.ts
3693
4865
  import { spawn } from "child_process";
@@ -4370,7 +5542,7 @@ function registerDbCommands(program2) {
4370
5542
  db2.name,
4371
5543
  getTypeLabel(db2.type),
4372
5544
  getStatusBadge(db2.status),
4373
- formatDate4(db2.created)
5545
+ formatDate5(db2.created)
4374
5546
  ])
4375
5547
  );
4376
5548
  log("");
@@ -5226,7 +6398,7 @@ function findDatabase(databases, identifier) {
5226
6398
  function generateSlug2(name) {
5227
6399
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
5228
6400
  }
5229
- function formatDate4(date) {
6401
+ function formatDate5(date) {
5230
6402
  const d = new Date(date);
5231
6403
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
5232
6404
  }
@@ -5312,7 +6484,78 @@ import {
5312
6484
  import { tmpdir } from "os";
5313
6485
  import { basename, dirname, join as join3 } from "path";
5314
6486
  import { promisify } from "util";
5315
- import open3 from "open";
6487
+ import open4 from "open";
6488
+
6489
+ // src/lib/entitlement-remedy.ts
6490
+ var planKeyOf = (p) => p.planKey ?? p.key ?? "";
6491
+ var addonKeyOf = (a) => a.addonKey ?? a.key ?? "";
6492
+ function nextPlanForRequested(requested) {
6493
+ const r = (requested ?? "free").trim().toLowerCase();
6494
+ if (!r || r === "free") return "shared";
6495
+ if (r === "shared") return "shared";
6496
+ if (r === "dedicated" || r === "dedicated_small") return "dedicated_small";
6497
+ if (r === "dedicated_medium") return "dedicated_medium";
6498
+ if (r === "dedicated_large") return "dedicated_large";
6499
+ return r;
6500
+ }
6501
+ function resolveEntitlementRemedy(failedKey, catalog, opts) {
6502
+ const plans = catalog?.plans ?? [];
6503
+ const addons = catalog?.addons ?? [];
6504
+ if (failedKey?.startsWith("app.shared")) {
6505
+ const sharedPlan = plans.find((p) => p.quantityAware);
6506
+ const targetKey = sharedPlan ? planKeyOf(sharedPlan) : "shared";
6507
+ const next = (opts?.currentSharedQuantity ?? 1) + 1;
6508
+ return {
6509
+ kind: "plan_quantity",
6510
+ failedKey,
6511
+ targetKey,
6512
+ targetName: sharedPlan?.name,
6513
+ priceHalalas: sharedPlan?.priceHalalas,
6514
+ command: `tarout billing plan:quantity ${next} --wait`,
6515
+ hint: `Your ${sharedPlan?.name ?? "Starter"} plan is quantity-aware \u2014 add an app slot by raising the plan quantity to ${next}.`
6516
+ };
6517
+ }
6518
+ if (failedKey?.startsWith("app.dedicated") || failedKey?.startsWith("host.")) {
6519
+ const requested = opts?.requestedPlan;
6520
+ const target2 = requested && requested.toLowerCase().startsWith("dedicated") ? nextPlanForRequested(requested) : "dedicated_small";
6521
+ const plan2 = plans.find((p) => planKeyOf(p) === target2);
6522
+ return {
6523
+ kind: "plan",
6524
+ failedKey,
6525
+ targetKey: target2,
6526
+ targetName: plan2?.name,
6527
+ priceHalalas: plan2?.priceHalalas,
6528
+ command: `tarout billing upgrade ${target2} --wait`,
6529
+ hint: `A dedicated host slot is required \u2014 upgrade to ${plan2?.name ?? target2}.`
6530
+ };
6531
+ }
6532
+ if (failedKey?.startsWith("db.") || failedKey?.startsWith("storage.") || failedKey?.startsWith("domain") || failedKey?.startsWith("email")) {
6533
+ const matched = addons.find(
6534
+ (a) => a.grants?.some((g) => g.entitlementKey === failedKey)
6535
+ ) ?? addons.find((a) => addonKeyOf(a) === failedKey.replace(/\.slots$/, ""));
6536
+ const targetKey = matched ? addonKeyOf(matched) : failedKey.replace(/\.slots$/, "");
6537
+ return {
6538
+ kind: "addon",
6539
+ failedKey,
6540
+ targetKey,
6541
+ targetName: matched?.name,
6542
+ priceHalalas: matched?.priceHalalas,
6543
+ command: `tarout billing addon:buy ${targetKey} --wait`,
6544
+ hint: `Add a ${matched?.name ?? targetKey} slot with an addon purchase.`
6545
+ };
6546
+ }
6547
+ const target = nextPlanForRequested(opts?.requestedPlan);
6548
+ const plan = plans.find((p) => planKeyOf(p) === target);
6549
+ return {
6550
+ kind: "plan",
6551
+ failedKey,
6552
+ targetKey: target,
6553
+ targetName: plan?.name,
6554
+ priceHalalas: plan?.priceHalalas,
6555
+ command: `tarout billing upgrade ${target} --wait`,
6556
+ hint: `Upgrade to a paid plan (${plan?.name ?? target}) to continue.`
6557
+ };
6558
+ }
5316
6559
 
5317
6560
  // src/lib/websocket.ts
5318
6561
  import WebSocket from "ws";
@@ -5477,7 +6720,7 @@ async function authenticateViaBrowser(action, apiUrl) {
5477
6720
  log(
5478
6721
  action === "register" ? "Opening browser to create your account..." : "Opening browser to authenticate..."
5479
6722
  );
5480
- await open3(authUrl);
6723
+ await open4(authUrl);
5481
6724
  const _spinner = startSpinner(
5482
6725
  action === "register" ? "Waiting for account creation..." : "Waiting for authentication..."
5483
6726
  );
@@ -6093,48 +7336,37 @@ function formatPlanPrice(priceHalalas) {
6093
7336
  return `${(priceHalalas / 100).toFixed(2)} SAR/mo`;
6094
7337
  }
6095
7338
  async function runInlineUpgrade(client, planKey) {
6096
- const { pollCheckoutUntilTerminal } = await import("./billing-GUA4S2Y4.js");
6097
7339
  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, {
7340
+ const result = await performBillingChange(client, {
7341
+ kind: "plan",
7342
+ planKey,
7343
+ wait: true,
6120
7344
  timeoutMs: 6e5,
6121
- intervalMs: 4e3
7345
+ openBrowser: isJsonMode() ? void 0 : async (url) => {
7346
+ await open4(url);
7347
+ },
7348
+ onCheckoutOpened: ({ orderId, paymentUrl }) => {
7349
+ log("");
7350
+ log("Open this URL to complete payment:");
7351
+ log(` ${colors.cyan(paymentUrl)}`);
7352
+ log(`Order ID: ${colors.dim(orderId)}`);
7353
+ }
6122
7354
  });
6123
- if (final.status === "PAID") {
6124
- succeedSpinner("Payment confirmed.");
7355
+ if (result.status === "applied" || result.status === "paid") {
7356
+ succeedSpinner("Plan applied.");
6125
7357
  return true;
6126
7358
  }
6127
7359
  failSpinner(
6128
- final.status === "PENDING" ? "Payment still pending after 10 minutes." : `Payment ${final.status.toLowerCase()}.`
7360
+ 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
7361
  );
6130
- if (final.failureReason) {
6131
- log(colors.error(final.failureReason));
7362
+ if (result.failureReason) log(colors.error(result.failureReason));
7363
+ if (result.orderId) {
7364
+ log(
7365
+ colors.dim(
7366
+ `Resume later with: tarout billing wait ${result.orderId.slice(0, 8)} --timeout 600`
7367
+ )
7368
+ );
6132
7369
  }
6133
- log(
6134
- colors.dim(
6135
- `Resume later with: tarout billing wait ${orderId.slice(0, 8)} --timeout 600`
6136
- )
6137
- );
6138
7370
  return false;
6139
7371
  }
6140
7372
  async function getAppPlanChoices(client, preloadedOptions) {
@@ -6189,13 +7421,7 @@ function isEntitlementError(err) {
6189
7421
  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
7422
  }
6191
7423
  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;
7424
+ return nextPlanForRequested(requested);
6199
7425
  }
6200
7426
  var ENTITLEMENT_LABELS = {
6201
7427
  "app.free.slots": "Free app slot",
@@ -6233,6 +7459,22 @@ function extractEntitlementKeyFromError(err) {
6233
7459
  const m = msg.match(/Plan limit reached for ([\w.]+)/i);
6234
7460
  return m?.[1];
6235
7461
  }
7462
+ async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
7463
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
7464
+ const failedKey = extractEntitlementKeyFromError(err);
7465
+ const catalog = await fetchCatalogSafely(client);
7466
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
7467
+ outputError("NEEDS_UPGRADE", message, {
7468
+ failedEntitlementKey: failedKey,
7469
+ remedyKind: remedy.kind,
7470
+ // `suggestedPlan` retained for back-compat; now correct for every gate
7471
+ // (the plan key, addon key, or quantity-aware plan to act on).
7472
+ suggestedPlan: remedy.targetKey,
7473
+ suggestedTarget: remedy.targetKey,
7474
+ nextCommand: remedy.command,
7475
+ hint: `${remedy.hint} Then retry: ${retryCommand}.`
7476
+ });
7477
+ }
6236
7478
  async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
6237
7479
  const catalog = await fetchCatalogSafely(client);
6238
7480
  const allPlans = catalog?.plans ?? [];
@@ -6376,7 +7618,7 @@ async function openGitProviderSetup() {
6376
7618
  log(
6377
7619
  `No GitHub account is required if you keep using ${colors.dim("--source upload")}.`
6378
7620
  );
6379
- await open3(url);
7621
+ await open4(url);
6380
7622
  }
6381
7623
  async function configureOptionalResources(client, profile, app, options, inspection) {
6382
7624
  const database = await resolveDatabaseChoice(options, inspection);
@@ -6787,28 +8029,58 @@ function resolveExplicitStorageRef(candidates, ref) {
6787
8029
  }
6788
8030
  return match;
6789
8031
  }
8032
+ function emitReuseNotice(payload) {
8033
+ if (isJsonMode()) {
8034
+ outputJsonLine({ type: "event", event: payload.event, ...payload.details });
8035
+ return;
8036
+ }
8037
+ log(colors.warn(payload.humanMessage));
8038
+ if (payload.humanHint) log(colors.dim(` ${payload.humanHint}`));
8039
+ }
6790
8040
  async function finalizeDatabaseReuse(client, candidate, kind, requestedPlan, planExplicit) {
6791
8041
  let plan = candidate.plan;
6792
8042
  let upgraded = false;
6793
8043
  if (planExplicit && requestedPlan) {
6794
8044
  const cmp = compareResourcePlan(requestedPlan, candidate.plan);
8045
+ const upgradeCommand = `tarout db upgrade ${candidate.name} --plan ${requestedPlan.toLowerCase()}`;
6795
8046
  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;
8047
+ if (isJsonMode() || shouldSkipConfirmation()) {
8048
+ emitReuseNotice({
8049
+ event: "reuse_plan_unchanged",
8050
+ humanMessage: `${formatDatabaseKind(kind)} ${candidate.name} stays on ${candidate.plan}; requested ${requestedPlan} was not applied automatically.`,
8051
+ humanHint: `Upgrade explicitly with: ${upgradeCommand}`,
8052
+ details: {
8053
+ resourceType: "database",
8054
+ name: candidate.name,
8055
+ currentPlan: candidate.plan,
8056
+ requestedPlan,
8057
+ nextCommand: upgradeCommand,
8058
+ hint: "Database plan upgrades are billing-mutating and may require checkout; run the command above to upgrade."
8059
+ }
8060
+ });
8061
+ } else {
8062
+ const ok = await confirmUpgrade(
8063
+ `${formatDatabaseKind(kind)} ${candidate.name} is on ${candidate.plan}. Upgrade to ${requestedPlan} before attaching?`,
8064
+ true,
8065
+ kind === "postgres" ? "upgrade_postgres" : "upgrade_mysql"
8066
+ );
8067
+ if (ok) {
8068
+ await runDatabaseUpgrade(client, kind, candidate.id, requestedPlan);
8069
+ plan = requestedPlan;
8070
+ upgraded = true;
8071
+ }
6805
8072
  }
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
- );
8073
+ } else if (cmp < 0) {
8074
+ emitReuseNotice({
8075
+ event: "reuse_plan_lower",
8076
+ humanMessage: `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`,
8077
+ details: {
8078
+ resourceType: "database",
8079
+ name: candidate.name,
8080
+ currentPlan: candidate.plan,
8081
+ requestedPlan
8082
+ }
8083
+ });
6812
8084
  }
6813
8085
  }
6814
8086
  return {
@@ -6824,16 +8096,10 @@ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplic
6824
8096
  let upgraded = false;
6825
8097
  if (planExplicit && requestedPlan) {
6826
8098
  const cmp = compareResourcePlan(requestedPlan, candidate.plan);
8099
+ const upgradeCommand = `tarout storage upgrade ${candidate.name} --plan ${requestedPlan.toLowerCase()}`;
6827
8100
  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") {
8101
+ const isPaidUpgradeable = requestedPlan === "STARTER" || requestedPlan === "STANDARD" || requestedPlan === "PRO";
8102
+ if (candidate.plan === "FREE" && isPaidUpgradeable && !isJsonMode() && !shouldSkipConfirmation()) {
6837
8103
  const ok = await confirmUpgrade(
6838
8104
  `Storage bucket ${candidate.name} is on FREE. Upgrade to ${requestedPlan} before attaching?`,
6839
8105
  true,
@@ -6844,13 +8110,32 @@ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplic
6844
8110
  plan = requestedPlan;
6845
8111
  upgraded = true;
6846
8112
  }
8113
+ } else {
8114
+ emitReuseNotice({
8115
+ event: "reuse_plan_unchanged",
8116
+ humanMessage: `Storage bucket ${candidate.name} stays on ${candidate.plan}; requested ${requestedPlan} was not applied automatically.`,
8117
+ humanHint: `Upgrade explicitly with: ${upgradeCommand}`,
8118
+ details: {
8119
+ resourceType: "storage",
8120
+ name: candidate.name,
8121
+ currentPlan: candidate.plan,
8122
+ requestedPlan,
8123
+ nextCommand: upgradeCommand,
8124
+ hint: "Storage plan upgrades are billing-mutating; run the command above to upgrade."
8125
+ }
8126
+ });
6847
8127
  }
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
- );
8128
+ } else if (cmp < 0) {
8129
+ emitReuseNotice({
8130
+ event: "reuse_plan_lower",
8131
+ humanMessage: `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`,
8132
+ details: {
8133
+ resourceType: "storage",
8134
+ name: candidate.name,
8135
+ currentPlan: candidate.plan,
8136
+ requestedPlan
8137
+ }
8138
+ });
6854
8139
  }
6855
8140
  }
6856
8141
  return {
@@ -6862,9 +8147,6 @@ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplic
6862
8147
  };
6863
8148
  }
6864
8149
  async function confirmUpgrade(message, defaultValue, field) {
6865
- if (isJsonMode() || shouldSkipConfirmation()) {
6866
- return false;
6867
- }
6868
8150
  return confirm(message, defaultValue, {
6869
8151
  field,
6870
8152
  flag: "--yes (default attaches at current plan; pass --database-plan/--storage-plan with --yes to force an upgrade)"
@@ -7301,11 +8583,12 @@ function registerDeployCommands(program2) {
7301
8583
  if (isEntitlementError(err)) {
7302
8584
  const message = err instanceof Error ? err.message : "Plan upgrade required";
7303
8585
  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
- });
8586
+ await emitNeedsUpgrade(
8587
+ getApiClient(),
8588
+ err,
8589
+ options.plan,
8590
+ "tarout deploy"
8591
+ );
7309
8592
  exit(ExitCode.PERMISSION_DENIED);
7310
8593
  }
7311
8594
  log("");
@@ -7316,11 +8599,12 @@ function registerDeployCommands(program2) {
7316
8599
  options.plan
7317
8600
  );
7318
8601
  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
- });
8602
+ await emitNeedsUpgrade(
8603
+ getApiClient(),
8604
+ err,
8605
+ options.plan,
8606
+ "tarout deploy"
8607
+ );
7324
8608
  exit(ExitCode.PERMISSION_DENIED);
7325
8609
  }
7326
8610
  box("Upgrade complete", [
@@ -7451,7 +8735,7 @@ function registerDeployCommands(program2) {
7451
8735
  colors.cyan(d.deploymentId.slice(0, 8)),
7452
8736
  getStatusBadge(d.status),
7453
8737
  d.title || colors.dim("-"),
7454
- formatDate5(d.createdAt)
8738
+ formatDate6(d.createdAt)
7455
8739
  ])
7456
8740
  );
7457
8741
  log("");
@@ -7643,10 +8927,10 @@ function registerDeployCommands(program2) {
7643
8927
  );
7644
8928
  log("");
7645
8929
  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]") : ""}`,
8930
+ name: `${colors.cyan(d.deploymentId.slice(0, 8))} - ${d.title || "Deployment"} (${formatDate6(d.createdAt)})${index === 0 ? colors.dim(" [current]") : ""}`,
7647
8931
  value: d.deploymentId
7648
8932
  }));
7649
- const { select: select2 } = await import("./prompts-QQ2FZKQT.js");
8933
+ const { select: select2 } = await import("./prompts-JH6YBHHV.js");
7650
8934
  targetDeploymentId = await select2(
7651
8935
  "Select deployment:",
7652
8936
  choices,
@@ -7662,10 +8946,10 @@ function registerDeployCommands(program2) {
7662
8946
  log(` Deployment: ${colors.cyan(targetDeploymentId.slice(0, 8))}`);
7663
8947
  log(` Title: ${targetDeployment?.title || "Deployment"}`);
7664
8948
  log(
7665
- ` Created: ${targetDeployment ? formatDate5(targetDeployment.createdAt) : colors.dim("unknown")}`
8949
+ ` Created: ${targetDeployment ? formatDate6(targetDeployment.createdAt) : colors.dim("unknown")}`
7666
8950
  );
7667
8951
  log("");
7668
- const { confirm: confirm2 } = await import("./prompts-QQ2FZKQT.js");
8952
+ const { confirm: confirm2 } = await import("./prompts-JH6YBHHV.js");
7669
8953
  const confirmed = await confirm2(
7670
8954
  "Are you sure you want to rollback?",
7671
8955
  false,
@@ -7787,7 +9071,7 @@ function findApp3(apps, identifier) {
7787
9071
  (app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
7788
9072
  );
7789
9073
  }
7790
- function formatDate5(date) {
9074
+ function formatDate6(date) {
7791
9075
  const d = new Date(date);
7792
9076
  return d.toLocaleString("en-US", {
7793
9077
  month: "short",
@@ -8428,7 +9712,7 @@ function registerDomainsCommands(program2) {
8428
9712
  d.source === "purchased" ? colors.info("purchased") : colors.dim("external"),
8429
9713
  formatStatus(d.status),
8430
9714
  formatCfStatus(d.cloudflareZoneStatus),
8431
- d.source === "purchased" && d.expiryDate ? formatDate6(d.expiryDate) : colors.dim("-"),
9715
+ d.source === "purchased" && d.expiryDate ? formatDate7(d.expiryDate) : colors.dim("-"),
8432
9716
  d.source === "purchased" ? d.autoRenew ? colors.success("on") : colors.warn("off") : colors.dim("-")
8433
9717
  ])
8434
9718
  );
@@ -10348,7 +11632,7 @@ function formatCfStatus(status) {
10348
11632
  return status;
10349
11633
  }
10350
11634
  }
10351
- function formatDate6(dateValue) {
11635
+ function formatDate7(dateValue) {
10352
11636
  try {
10353
11637
  const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
10354
11638
  return date.toISOString().split("T")[0] || "";
@@ -10406,7 +11690,7 @@ function registerEnvCommands(program2) {
10406
11690
  colors.cyan(v.key),
10407
11691
  options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
10408
11692
  v.isSecret ? colors.warn("Yes") : "No",
10409
- formatDate7(v.updatedAt)
11693
+ formatDate8(v.updatedAt)
10410
11694
  ])
10411
11695
  );
10412
11696
  log("");
@@ -10789,7 +12073,7 @@ ${colors.bold(key)}: ${maskValue(val.value || String(v))}
10789
12073
  } else {
10790
12074
  const _raw = await import("process");
10791
12075
  log('Enter JSON key-value object (e.g. {"KEY":"value"}):');
10792
- const input2 = await (await import("./prompts-QQ2FZKQT.js")).input(
12076
+ const input2 = await (await import("./prompts-JH6YBHHV.js")).input(
10793
12077
  "JSON:"
10794
12078
  );
10795
12079
  vars = JSON.parse(input2);
@@ -10812,7 +12096,7 @@ ${colors.bold(key)}: ${maskValue(val.value || String(v))}
10812
12096
  try {
10813
12097
  if (!isLoggedIn()) throw new AuthError();
10814
12098
  if (!shouldSkipConfirmation()) {
10815
- const { confirm: confirmFn } = await import("./prompts-QQ2FZKQT.js");
12099
+ const { confirm: confirmFn } = await import("./prompts-JH6YBHHV.js");
10816
12100
  const ok = await confirmFn(
10817
12101
  `Delete ${keys.length} variable(s)?`,
10818
12102
  false
@@ -10851,7 +12135,7 @@ ${colors.bold(key)}: ${maskValue(val.value || String(v))}
10851
12135
  );
10852
12136
  if (!app) throw new NotFoundError("Application", appIdentifier);
10853
12137
  if (!shouldSkipConfirmation()) {
10854
- const { confirm: confirmFn } = await import("./prompts-QQ2FZKQT.js");
12138
+ const { confirm: confirmFn } = await import("./prompts-JH6YBHHV.js");
10855
12139
  const ok = await confirmFn(
10856
12140
  `Copy env vars from ${fromEnvId} to ${toEnvId}?`,
10857
12141
  false
@@ -10885,7 +12169,7 @@ function maskValue(value) {
10885
12169
  if (value.length <= 4) return "****";
10886
12170
  return `${value.slice(0, 2)}****${value.slice(-2)}`;
10887
12171
  }
10888
- function formatDate7(date) {
12172
+ function formatDate8(date) {
10889
12173
  const d = new Date(date);
10890
12174
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
10891
12175
  }
@@ -11542,7 +12826,7 @@ function registerKeysCommands(program2) {
11542
12826
  k.name,
11543
12827
  colors.dim(`${(k.fingerprint || "").slice(0, 20)}...`),
11544
12828
  k.isDefault ? colors.success("*") : "",
11545
- formatDate8(k.createdAt)
12829
+ formatDate9(k.createdAt)
11546
12830
  ])
11547
12831
  );
11548
12832
  log("");
@@ -11756,7 +13040,7 @@ function findKey(keys, identifier) {
11756
13040
  (k) => (k.keyId || k.id) === identifier || (k.keyId || k.id || "").startsWith(identifier) || k.name.toLowerCase() === lower
11757
13041
  );
11758
13042
  }
11759
- function formatDate8(date) {
13043
+ function formatDate9(date) {
11760
13044
  if (!date) return colors.dim("-");
11761
13045
  return new Date(date).toLocaleDateString("en-US", {
11762
13046
  month: "short",
@@ -12849,7 +14133,7 @@ function registerOrgsCommands(program2) {
12849
14133
  m.user?.name || m.name || colors.dim("-"),
12850
14134
  m.user?.email || m.email || colors.dim("-"),
12851
14135
  formatRole(m.role),
12852
- formatDate9(m.createdAt)
14136
+ formatDate10(m.createdAt)
12853
14137
  ])
12854
14138
  );
12855
14139
  log("");
@@ -12885,7 +14169,7 @@ function registerOrgsCommands(program2) {
12885
14169
  inv.email,
12886
14170
  formatRole(inv.role),
12887
14171
  inv.status || "pending",
12888
- formatDate9(inv.createdAt)
14172
+ formatDate10(inv.createdAt)
12889
14173
  ])
12890
14174
  );
12891
14175
  log("");
@@ -13373,7 +14657,7 @@ function formatRole(role) {
13373
14657
  };
13374
14658
  return map[role] || role;
13375
14659
  }
13376
- function formatDate9(date) {
14660
+ function formatDate10(date) {
13377
14661
  if (!date) return colors.dim("-");
13378
14662
  return new Date(date).toLocaleDateString("en-US", {
13379
14663
  month: "short",
@@ -13659,7 +14943,7 @@ function registerProjectsCommands(program2) {
13659
14943
  }
13660
14944
 
13661
14945
  // src/commands/providers.ts
13662
- import open4 from "open";
14946
+ import open5 from "open";
13663
14947
  function registerProvidersCommands(program2) {
13664
14948
  const providers = program2.command("providers").description("Manage Git providers (GitHub, GitLab, Bitbucket)");
13665
14949
  providers.command("list").alias("ls").description("List all connected Git providers").action(async () => {
@@ -13776,7 +15060,7 @@ function registerProvidersCommands(program2) {
13776
15060
  log(
13777
15061
  "Complete the GitHub browser flow, then connect the repository to your app."
13778
15062
  );
13779
- await open4(url);
15063
+ await open5(url);
13780
15064
  } catch (err) {
13781
15065
  handleError(err);
13782
15066
  }
@@ -14581,7 +15865,7 @@ function registerServersCommands(program2) {
14581
15865
  s.serverSize || s.size || colors.dim("-"),
14582
15866
  getStatusBadge(s.status || "unknown"),
14583
15867
  s.publicIp || s.ip || colors.dim("-"),
14584
- formatDate10(s.createdAt)
15868
+ formatDate11(s.createdAt)
14585
15869
  ])
14586
15870
  );
14587
15871
  log("");
@@ -14743,7 +16027,7 @@ function registerServersCommands(program2) {
14743
16027
  log(` Private IP: ${details.privateIp || colors.dim("-")}`);
14744
16028
  log("");
14745
16029
  if (details.createdAt) {
14746
- log(` Created: ${formatDate10(details.createdAt)}`);
16030
+ log(` Created: ${formatDate11(details.createdAt)}`);
14747
16031
  }
14748
16032
  log("");
14749
16033
  } catch (err) {
@@ -15012,7 +16296,7 @@ function registerServersCommands(program2) {
15012
16296
  s.name || colors.dim("-"),
15013
16297
  s.status || colors.dim("-"),
15014
16298
  formatBytes4(s.diskSizeGb ? s.diskSizeGb * 1024 * 1024 * 1024 : 0),
15015
- formatDate10(s.createdAt)
16299
+ formatDate11(s.createdAt)
15016
16300
  ])
15017
16301
  );
15018
16302
  log("");
@@ -15536,7 +16820,7 @@ function registerServersCommands(program2) {
15536
16820
  colors.cyan(ip.ip || ip.address || "-"),
15537
16821
  ip.region || "-",
15538
16822
  ip.assignedServerId ? colors.dim(ip.assignedServerId.slice(0, 8)) : colors.dim("unassigned"),
15539
- formatDate10(ip.createdAt)
16823
+ formatDate11(ip.createdAt)
15540
16824
  ])
15541
16825
  );
15542
16826
  log("");
@@ -16390,7 +17674,7 @@ function findServer(servers, identifier) {
16390
17674
  (s) => (s.id || s.serverId) === identifier || (s.id || s.serverId || "").startsWith(identifier) || (s.name || "").toLowerCase() === lower
16391
17675
  );
16392
17676
  }
16393
- function formatDate10(date) {
17677
+ function formatDate11(date) {
16394
17678
  if (!date) return colors.dim("-");
16395
17679
  return new Date(date).toLocaleDateString("en-US", {
16396
17680
  month: "short",
@@ -16576,7 +17860,7 @@ function registerStorageCommands(program2) {
16576
17860
  b.plan || colors.dim("free"),
16577
17861
  b.region || colors.dim("-"),
16578
17862
  b.publicAccess ? colors.success("yes") : colors.dim("no"),
16579
- formatDate11(b.createdAt)
17863
+ formatDate12(b.createdAt)
16580
17864
  ])
16581
17865
  );
16582
17866
  log("");
@@ -16759,7 +18043,7 @@ function registerStorageCommands(program2) {
16759
18043
  log(` ${colors.dim("Not available")}`);
16760
18044
  }
16761
18045
  log("");
16762
- log(` Created: ${formatDate11(bucket.createdAt)}`);
18046
+ log(` Created: ${formatDate12(bucket.createdAt)}`);
16763
18047
  log("");
16764
18048
  } catch (err) {
16765
18049
  handleError(err);
@@ -16809,7 +18093,7 @@ function registerStorageCommands(program2) {
16809
18093
  f.name || f.key || "",
16810
18094
  formatBytes5(f.size || f.sizeBytes || 0),
16811
18095
  f.contentType || colors.dim("-"),
16812
- formatDate11(f.updated || f.updatedAt || f.lastModified)
18096
+ formatDate12(f.updated || f.updatedAt || f.lastModified)
16813
18097
  ])
16814
18098
  );
16815
18099
  log("");
@@ -17250,7 +18534,7 @@ function findBucket(buckets, identifier) {
17250
18534
  (b) => (b.bucketId || b.id) === identifier || (b.bucketId || b.id || "").startsWith(identifier) || b.name.toLowerCase() === lower
17251
18535
  );
17252
18536
  }
17253
- function formatDate11(date) {
18537
+ function formatDate12(date) {
17254
18538
  if (!date) return colors.dim("-");
17255
18539
  return new Date(date).toLocaleDateString("en-US", {
17256
18540
  month: "short",
@@ -17312,7 +18596,7 @@ ${colors.warn(`${unread.count} unread ticket${unread.count === 1 ? "" : "s"}`)}`
17312
18596
  formatStatus2(t.status),
17313
18597
  formatPriority(t.priority),
17314
18598
  t.category || colors.dim("-"),
17315
- formatDate12(t.createdAt)
18599
+ formatDate13(t.createdAt)
17316
18600
  ])
17317
18601
  );
17318
18602
  log("");
@@ -17449,7 +18733,7 @@ ${colors.warn(`${unread.count} unread ticket${unread.count === 1 ? "" : "s"}`)}`
17449
18733
  log(` Status: ${formatStatus2(ticket.status)}`);
17450
18734
  log(` Priority: ${formatPriority(ticket.priority)}`);
17451
18735
  log(` Category: ${ticket.category || colors.dim("-")}`);
17452
- log(` Created: ${formatDate12(ticket.createdAt)}`);
18736
+ log(` Created: ${formatDate13(ticket.createdAt)}`);
17453
18737
  log("");
17454
18738
  if (ticket.description) {
17455
18739
  log(colors.bold("Description"));
@@ -17701,7 +18985,7 @@ function formatPriority(priority) {
17701
18985
  };
17702
18986
  return map[priority] || priority;
17703
18987
  }
17704
- function formatDate12(date) {
18988
+ function formatDate13(date) {
17705
18989
  if (!date) return colors.dim("-");
17706
18990
  return new Date(date).toLocaleDateString("en-US", {
17707
18991
  month: "short",
@@ -17921,11 +19205,7 @@ function registerUpCommand(program2) {
17921
19205
  if (isEntitlementError(err)) {
17922
19206
  const message = err instanceof Error ? err.message : "Plan upgrade required";
17923
19207
  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
- });
19208
+ await emitNeedsUpgrade(client, err, options.plan, "tarout up");
17929
19209
  exit(ExitCode.PERMISSION_DENIED);
17930
19210
  }
17931
19211
  log("");
@@ -17936,11 +19216,7 @@ function registerUpCommand(program2) {
17936
19216
  options.plan
17937
19217
  );
17938
19218
  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
- });
19219
+ await emitNeedsUpgrade(client, err, options.plan, "tarout up");
17944
19220
  exit(ExitCode.PERMISSION_DENIED);
17945
19221
  }
17946
19222
  box("Upgrade complete", [
@@ -18149,7 +19425,7 @@ function registerWalletCommands(program2) {
18149
19425
  table(
18150
19426
  ["DATE", "TYPE", "AMOUNT", "BALANCE", "DESCRIPTION"],
18151
19427
  entries.map((e) => [
18152
- formatDate13(e.createdAt),
19428
+ formatDate14(e.createdAt),
18153
19429
  formatType(e.type),
18154
19430
  formatAmount(e.amountHalalas),
18155
19431
  formatAmount(e.balanceAfterHalalas),
@@ -18250,7 +19526,7 @@ function formatType(type) {
18250
19526
  };
18251
19527
  return map[type] || type;
18252
19528
  }
18253
- function formatDate13(iso) {
19529
+ function formatDate14(iso) {
18254
19530
  return new Date(iso).toLocaleDateString("en-US", {
18255
19531
  month: "short",
18256
19532
  day: "numeric"