@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/{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 +1499 -223
- 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.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-
|
|
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;
|
|
@@ -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
|
-
|
|
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("
|
|
3570
|
-
const
|
|
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(
|
|
4704
|
+
outputData(data);
|
|
3577
4705
|
return;
|
|
3578
4706
|
}
|
|
3579
|
-
const list = Array.isArray(
|
|
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
|
-
["
|
|
3588
|
-
list.map((
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
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
|
-
|
|
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("
|
|
3604
|
-
const
|
|
3605
|
-
|
|
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(
|
|
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(
|
|
3615
|
-
|
|
3616
|
-
|
|
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
|
-
|
|
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("
|
|
3627
|
-
const
|
|
3628
|
-
|
|
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(
|
|
3631
|
-
if (isJsonMode())
|
|
3632
|
-
|
|
3633
|
-
|
|
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
|
-
|
|
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("
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, {
|
|
7340
|
+
const result = await performBillingChange(client, {
|
|
7341
|
+
kind: "plan",
|
|
7342
|
+
planKey,
|
|
7343
|
+
wait: true,
|
|
6120
7344
|
timeoutMs: 6e5,
|
|
6121
|
-
|
|
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 (
|
|
6124
|
-
succeedSpinner("
|
|
7355
|
+
if (result.status === "applied" || result.status === "paid") {
|
|
7356
|
+
succeedSpinner("Plan applied.");
|
|
6125
7357
|
return true;
|
|
6126
7358
|
}
|
|
6127
7359
|
failSpinner(
|
|
6128
|
-
|
|
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 (
|
|
6131
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
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
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
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
|
-
|
|
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") {
|
|
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
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
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
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
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
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
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
|
-
|
|
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"} (${
|
|
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-
|
|
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 ?
|
|
8949
|
+
` Created: ${targetDeployment ? formatDate6(targetDeployment.createdAt) : colors.dim("unknown")}`
|
|
7666
8950
|
);
|
|
7667
8951
|
log("");
|
|
7668
|
-
const { confirm: confirm2 } = await import("./prompts-
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19529
|
+
function formatDate14(iso) {
|
|
18254
19530
|
return new Date(iso).toLocaleDateString("en-US", {
|
|
18255
19531
|
month: "short",
|
|
18256
19532
|
day: "numeric"
|