@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/{api-QAKANRFX.js → api-QVUO7EWV.js} +2 -2
- package/dist/{chunk-CJMIX35A.js → chunk-DI66W4S5.js} +1 -1
- package/dist/{chunk-KL3JNPAY.js → chunk-K7JK5HIL.js} +6 -1
- package/dist/{chunk-NHNK5ZQ5.js → chunk-Y6TWR3XZ.js} +1 -1
- package/dist/index.js +1429 -217
- package/dist/{prompts-QQ2FZKQT.js → prompts-JH6YBHHV.js} +2 -2
- package/package.json +1 -1
- package/dist/billing-GUA4S2Y4.js +0 -12
- package/dist/chunk-BS6DFVSU.js +0 -998
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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("
|
|
3570
|
-
const
|
|
3571
|
-
|
|
3572
|
-
|
|
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(
|
|
4621
|
+
outputData(data);
|
|
3577
4622
|
return;
|
|
3578
4623
|
}
|
|
3579
|
-
const
|
|
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
|
-
["
|
|
3588
|
-
list.map((
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
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
|
-
|
|
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("
|
|
3604
|
-
const
|
|
3605
|
-
|
|
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(
|
|
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(
|
|
3615
|
-
|
|
3616
|
-
|
|
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
|
-
|
|
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("
|
|
3627
|
-
const
|
|
3628
|
-
|
|
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(
|
|
3631
|
-
if (isJsonMode())
|
|
3632
|
-
|
|
3633
|
-
|
|
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
|
-
|
|
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("
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
log(
|
|
3666
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
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
|
-
|
|
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 (
|
|
6124
|
-
succeedSpinner("
|
|
7291
|
+
if (result.status === "applied" || result.status === "paid") {
|
|
7292
|
+
succeedSpinner("Plan applied.");
|
|
6125
7293
|
return true;
|
|
6126
7294
|
}
|
|
6127
7295
|
failSpinner(
|
|
6128
|
-
|
|
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 (
|
|
6131
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
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
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
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
|
-
|
|
6829
|
-
|
|
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
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
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
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
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
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
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
|
-
|
|
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"} (${
|
|
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-
|
|
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 ?
|
|
8885
|
+
` Created: ${targetDeployment ? formatDate6(targetDeployment.createdAt) : colors.dim("unknown")}`
|
|
7666
8886
|
);
|
|
7667
8887
|
log("");
|
|
7668
|
-
const { confirm: confirm2 } = await import("./prompts-
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19465
|
+
function formatDate14(iso) {
|
|
18254
19466
|
return new Date(iso).toLocaleDateString("en-US", {
|
|
18255
19467
|
month: "short",
|
|
18256
19468
|
day: "numeric"
|